เขียน Windows Service ด้วย F#

Dependencies

  • System.ServiceProcess
  • System.Configuration.Install

Service

เขียน Service โดย extend คลาสที่ชื่อ ServiceBase และ override OnStart() และ OnStop() ซึ่งเป็นฟังก์ชันที่จะถูกเรียกเมื่อมีการ Start/Stop service

1
2
3
4
5
6
7
8
9
10
11
12
type WindowsService() =
inherit ServiceBase()
let mutable service : IDisposable = null

override x.OnStart(args) =
let url = LoadAppConfig().Uri
service <- WebApp.Start<Startup.Startup>(url)
base.OnStart(args)

override x.OnStop() =
if obj.ReferenceEquals(null, service) |> not then service.Dispose()
base.OnStop()

Installer

Installer เป็นคลาสที่ใช้สำหรับระบุรายละเอียดของ Service เช่น ชื่อ Service วิธีการ Start จะให้ Start แบบ Auto หรือ Manual สามารถเขียน Installer โดย extend คลาส Installer และระบุ attribute RunInstaller ไว้ที่ชื่อคลาส

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[<RunInstaller(true)>]
type MyInstaller() as this =
inherit Installer()
do
let spi = new ServiceProcessInstaller()
let si = new ServiceInstaller()
spi.Account <- ServiceAccount.LocalSystem
spi.Username <- null
spi.Password <- null
si.DisplayName <- "MyService"
si.Description <- "MyService"
si.StartType <- ServiceStartMode.Automatic
si.ServiceName <- "MyService"
this.Installers.Add(spi) |> ignore
this.Installers.Add(si) |> ignore

Entry Point

หลังจากเขียน Service และ Installer แล้ว ใน EntryPoint ของโปรแกรม สามารถเรียก Service โดยใช้ Static Method Run ของคลาส ServiceBase

1
2
3
4
5
[<EntryPoint>]
let main argv =
use service = new WindowsService()
ServiceBase.Run(service)
0

Install / Uninstall Script

เมื่อ Compile โปรแกรมเป็น .exe แล้ว สามารถติดตั้งโปรแกรมให้รันเป็น Windows Service โดยใช้ utility ที่มาพร้อมกัน .Net Framework คือ installutil.exe

  • ใช้ option /I สำหรับติดตั้ง
  • ใช้ option /U สำหรับ Uninstall

Install Service

1
"C:\Windows\Microsoft.NET\Framework\v4.0.30319\installutil.exe" /I "%~dp0MyService.exe"

Uninstall ServiceName

1
"C:\Windows\Microsoft.NET\Framework\v4.0.30319\installutil.exe" /U "%~dp0MyService.exe"

การบุชื่อโปรแกรมจะต้องใช้ Absolute path จากตัวอย่างจะใช้ ตัวแปร %-dp0 เพื่อดึง path เต็มของโปรแกรม

อัปเดต String ด้วย Pointer

Note

String เป็น immutable type ที่ไม่มี public api สำหรับแก้ไขข้อมูล เราจึงไม่สามารถเปลี่ยนค่าใน memory ที่เก็บ string ได้ ในกรณีที่มีการ assign ค่าใหม่ CLR จะสร้าง string ชุดใหม่เก็บใน memory ตำแหน่งใหม่

Pointer

ใน C# ไม่มี type ไหนเป็น immutable ที่สมบูรณ์ เนื่องจากเราสามารถใช้ pointer เข้าถึงตำแหน่งใน memory ได้โดยตรง การใช้ pointer ใน C# ต้องทำใน unsafe block เท่านั้น

ตัวอย่าง

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Test {
public static string T() {
string s = "cat";
unsafe {
fixed (char* i = s) {
*i = 'b';
char* p2 = i + 1;
*p2 = 'o';
}
}
return s;
}
}
Test.T(); // bot

สร้าง Instance ด้วย Lambda Expression

Note

สามารถสร้าง instance ด้วย lambda expression โดยไม่ต้องสนใจ accessibility ของคลาส

ตัวอย่าง

ตัวอย่างนี้เป็นการใช้ expression ที่ประกาศไว้ใน namespace System.Linq.Expression

1
2
3
4
5
6
7
8
using System.Linq.Expressions
class C {
private C(){ Console.WriteLine("init.."); }
}
Expression<Func<C>> e =
Expression.Lambda<Func<C>>(Expression.New(typeof(C)),
Enumerable.Empty<ParameterExpression>());
Func<C> f = e.Compile();

ในโค้ดมีการใช้ Expression.New เพื่อสร้าง instance ของคลาส C คำสั่งนี้จะไม่มีการเช็ค accessibility ของคลาสโดยสังเกตจากการเรียกฟังก์ชั่น f ที่ได้จากการคอมไพล์ expression จะไม่เกิด error เกี่ยวกับ protection level ทั้งที่ constructor ของคลาส C ประกาศเป็น private

1
var c1 = f();

ผลลัพธ์

1
init..

จะต่างจากการสร้าง instance โดยใช้ new ซึ่ง compiler จะเช็ด accessibility ของคลาสเสมอ

1
var c2 = new C();

ผลลัพท์ คือ โค้ดไม่สามารถ compile โดยจะแสดง message ดังนี้

1
2
(1,11): error CS0122: `C.C()' is inaccessible due to its protection level
(2,13): (Location of the symbol related to previous error)

ฟังก์ชัน Fold กับ Reduce

ใน F# มีฟังก์ชั่นที่ใช้สำหรับ aggregate ข้อมูลคล้ายกับ Aggregate ของ LINQ คือ fold กับ reduce

Aggregate ใน C#.

1
2
3
var data = new int[] { 1,2,3};
var sum = data.Aggregate(0, (acc,x) => acc + x); // sum = 6
var pro = data.Aggregate(1, (acc,x) => acc * x); // pro = 6

Fold

1
2
3
4
open System.Linq
let data = [|1;2;3|]
let sum = Array.fold (fun acc x -> acc + x) 0 data // sum = 6
let pro = Array.fold (fun acc x -> acc * x) 1 data // pro = 6

ฟังก์ชั่น fold รับ parameter ทั้งหมด 3 ตัว คือ

  • folder คือ ฟังก์ชันสำหรับ transform หรือ update state
  • state คือ initial value หรือค่าเริ่มต้นที่จะถูกใช้ในฟังก์ชัน folder
  • input คือ ข้อมูลที่ต้องการ process

Reduce

1
2
3
4
5
6
7
8
let data = [1;2;3]
let empty = []

let sum1 = List.reduce(fun acc x -> acc + x) data // sum1 = 6
let sum2 = List.reduce(fun acc x -> acc + x) empty // ERROR: The input list was empty.

let sum3 = List.fold(fun acc x -> acc + x) 0 data // sum3 = 6
let sum4 = List.fold(fun acc x -> acc + x) 0 empty // sum4 = 0

ฟังก์ชั่น reduce จะคล้ายกับ fold ต่างกันที่เราไม่สามารถใส่ state หรือ initial value แบบ explicit ได้ fold จึงต้องดึง element ตัวแรกของ input เป็น initial value เสมอ ทำให้เกิดข้อจำกัด คือ ข้อมูล input จำเป็นต้องมีอย่างน้อย 1 element มิฉะนั้นจะเกิด error System.ArgumentException: The input list was empty

ความแตกต่างอีกข้อคือ initial value ที่ใสในฟังก์ชั่น fold สามารถเป็น type ใดก็ได้ ส่งผลให้ลัพท์สุดท้ายไม่จำเป็นต้องเป็น type เดียวกับ input element จะแตกต่างจาก reduce ที่ output ต้องเป็น type เดียวกับ input element เสมอ

1
[1 .. 3] |> List.fold (fun str n -> str + "," + (string n)) "" // ,1,2,3

Links

เขียน Entity Framework Code First ด้วย F#

Dependencies

ตัวอย่างนี้จะใช้ EF เชื่อมกับฐานข้อมูล PostgreSQL ซึ่งจะต้องใช้ dependencies สองตัว คือ EntityFramework และ Npgsql.EntityFramework สามารถติดตั้งผ่าน Package Manager Console ดังนี้

1
2
Install-Package EntityFramewok
Install-Package Npgsql.EntityFramework

Config

ในไฟล์ App.config ต้องเพิ่มแท็ก configSections system.Data entityFramework และ connectionString โดยชื่อ connection ในที่นี่จะใช้คำว่า production พร้อมระบุรายละเอียดของฐานข้อมูลไว้ใน attribute ชื่อ connectionString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<system.data>
<DbProviderFactories>
<remove invariant="Npgsql" />
<add name="Npgsql Data Provider" invariant="Npgsql" support="FF" description=".Net Framework Data Provider for Postgresql" type="Npgsql.NpgsqlFactory, Npgsql" />
</DbProviderFactories>
</system.data>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
<providers>
<provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" />
<provider invariantName="Npgsql" type="Npgsql.NpgsqlServices, Npgsql.EntityFramework" />
</providers>
</entityFramework>
<connectionStrings>
<add name="production" providerName="Npgsql" connectionString="Server=192.168.0.xxx;Port=5432;Database=DbName; User Id=xxx;Password=xxx;" />
</connectionStrings>
</configuration>

Models

EF Code First ต้องประกาศ Entity class ในตัวอย่างนี้จะมีเพียงคลาสเดียว คือ QUser ซึ่ง extend จาก abstract class ชื่อ Common ที่ประกาศ primary key ไว้

1
2
3
4
5
6
7
8
9
10
[<AbstractClass>]
type Common() =
[<Key>]
member val Id = 0 with get, set
type QUser() =
inherit Common()
member val FirstName = "" with set,get
member val LastName = "" with set,get
member val Position = "" with set,get
member val Email = "" with set,get

Context

Context ใน EF ใช้เป็น interface สำหรับ insert update delete และ query ข้อมูล โดยการประกาศ context ต้อง inherit class ชื่อ DbContext

1
2
3
4
5
6
7
type AppContext(connection:string) =
inherit DbContext(connection)
[<DefaultValue>]
val mutable private users: DbSet<QUser>
member this.Users
with get() = this.users
and set v = this.users <- v

AppContext มี parameter 1 ตัวชื่อ connection เพื่อใช้สำหรับส่งชื่อ connection ที่ระบุไว้ในไฟล์ App.config

Create Schema

เมื่อเขียน Entity class และ context ครบแล้ว ต่อไปคือการ generate ฐานข้อมูลให้ได้ table และ field ตรง ที่ตามที่ประกาศไว้ใน Entity class วิธีการคือ ให้เพิ่มโค้ด Database.SetInitializer ก่อนที่จะทำ operation ใด ๆ ก็ตามกับฐานข้อมูล

1
2
3
4
Database.SetInitializer<AppContext>
(new DropCreateDatabaseIfModelChanges<AppContext>());
use context = new AppContext("production")
let rs = context.Users.Where(fun x -> x.FirstName = "").FirstOrDefault()

จากตัวอย่างใช้วิธีส่งคำสั่ง new DropCreateDatabaseIfModelChanges<AppContext>() ซึ่งหมายความว่าให้ drop ฐานข้อมูลและสร้างใหม่ถ้าตรวจเจอว่า Entity class มีการแก้ไข

หลังจาก Generate schema เราสามารถ insert update ฐานข้อมูลได้โดยใช้ syntax แบบ LINQ method หรือผ่าน LINQ query ก็ได้ สำหรับ LINQ query จะมี syntax ที่แตกต่างจาก C# สามารถดูตัวอย่างได้ตามลิงค์ด้านล่างนี้

Links

Fizz Buzz โดยใช้ Reactive Extensions

Tools

Dependencies

ไลบรารี่ Reactive Extensions ของ .Net ชื่อ Rx-Main เมื่อตั้งตั้งแล้วจะได้ dll ทั้งหมด 4 ไฟล์ คือ System.Reactive.dll System.Reactive.Interfes.dll System.Reactive.Linq.dll และ System.Reactive.PlatformServices.dll เราสามารถระบุ dependencies นี้ไว้ในไฟล์ paket.dependencies

ไฟล์ paket.dependencies

1
2
source https://www.nuget.org/api/v2
nuget Rx-Main

ทำการติดตั้ง Rx-Main โดยใช้ paket ผ่านคำสั่งใช้คำสั่ง

1
paket install

โปรแกรม paket จะดาวโหลด dll ทั้งหมดไว้ในโฟลเดอร์ packages

Project

เมื่อตั้งตั้ง dependencies ครบแล้วทำการสร้างไฟล์ project โดยตั้งชื่อว่า FizzBuzz.xml และระบุ dependencies และ HintPath ไว้ในแท็ก ItemGroup

สำหรับไฟล์ Program.cs ที่เป็นโค้ดของโปรแกรมจะเก็บไว้ใน ItemGroup แยกอีกอัน

ไฟล์ FizzBuzz.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolVersion)\Microsoft.Common.props"
Condition="Exists('$(MSBuildExtensionPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"/>

<ItemGroup>
<Reference Include="System"/>
<Reference Include="System.Core"/>
<Reference Include="System.Reactive.Core">
<HintPath>packages/Rx-Core/lib/net45/System.Reactive.Core.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Interfaces">
<HintPath>packages/Rx-Interfaces/lib/net45/System.Reactive.Interfaces.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.Linq">
<HintPath>packages/Rx-Linq/lib/net45/System.Reactive.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Reactive.PlatformServices">
<HintPath>packages/Rx-PlatformServices/lib/net45/System.Reactive.PlatformServices.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs"/>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets"/>
</Project>

Source

ในโปรแกรมจะมี static เมธอดชื่อ Generate ข้างในมีการใช้ Observable ที่เป็น Reactive Extensions โดยมีการใช้ filter ทั้งหมด 4 ตัวคือ dividedByThree dividedByFive dividedByThreeAndFive และ simpleNumbers ในโปรแกรมมีการ compose filter ทั้งหมดรวมกันโดยใช้ฟังก์ชัน Merge

ไฟล์ Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Collections.Generic;
using System.Linq;
public class Program {
public static IEnumerable<string> Generate(int max) {
var result = new List<string>();
if (max <= 0) { return result; }
var observable = Observable.Range(1, max);
var dividedByThree = observable.Where(i => i % 3 == 0).Select(_ => "Fizz");
var dividedByFive = observable.Where(i => i % 5 == 0).Select(_ => "Buzz");
var dividedByThreeAndFive = observable.Where(i => i % 15 == 0).Select(_ => "FizzBuzz");
var simpleNumbers = observable.Where(i => i % 3 != 0 && i % 5 != 0).Select(i => i.ToString());
var specialCases = (dividedByThreeAndFive)
.Merge(dividedByThree)
.Merge(dividedByFive);
simpleNumbers.Merge(specialCases).Subscribe(s => result.Add(s));
return result;
}
public static void Main(String[] args) {
Generate(100).ToList().ForEach(Console.WriteLine);
}
}

Build

คอมไพล์โค้ดโดยใช้ xbuild ผ่านคำสั่ง

1
xbuild FizzBuzz.xml

xbuild จะสร้างไฟล์ FizzBuzz.exe เก็บไว้ในโพลเดอร์ bin/Debug

Run

1
mono bin/Debug/FizzBuzz.exe

Links

+ กับ StringBuilder

Concat String ด้วยเครื่องหมาย +

1
2
3
4
5
6
7
DateTime start = DateTime.Now;
string x = "";
for (int i=0; i < 100000; i++) {
x += "!";
}
DateTime end = DateTime.Now;
Console.WriteLine ("Time taken: {0}", end-start);

เนื่องจาก string เป็น immutable type ทุกครั้งที่ x += "!" ทำงานจะมีการสร้าง string ขึ้นใหม่ โดยคัดลอก string เดิมต่อกับเครื่องหมาย ! ไปเก็บใน memory ที่มีการ allocate ขึ้นใหม่ จากโค้ดข้างบนจะมีการคัดลอก string ถึง 100,000 ครั้ง แต่ละครั้งจะใช้เวลามากขึ้นเรื่อยๆ เนื่องจากจากขนาดของ string ยาวขึ้น

1
Time taken: 00:00:09.3561040

Concat String ด้วย StringBuilder

1
2
3
4
5
6
7
8
DateTime start = DateTime.Now;
StringBuilder builder = new StringBuilder();
for (int i=0; i < 100000; i++) {
builder.Append("!");
}
string x = builder.ToString();
DateTime end = DateTime.Now;
Console.WriteLine ("Time taken: {0}", end-start);

StringBuilder จะมี buffer อยู่ภายใน เมื่อมีการการต่อ string ด้วยฟังก์ชัน Append จะไม่จำเป็นต้องจอง memory ใหม่ทุกครั้ง แต่จะเกิดขึ้นเมื่อ memory หรือ buffer ที่มีอยู่ไม่พอ โดยจะเพิ่มขนาดของ buffer เป็นสองเท่าของขนาดเดิม จากโค้ดทั้งสองแบบ การใช้ StringBuilder จึงให้ประสิทธิภาพที่ดีกว่า

1
Time taken: 00:00:00.0527960

Links

User-Defined Conversion ใน C#

Implicit กับ Explicit Conversion

1
2
float f = 105.5f;
double d = f;

float f สามารถแปลงเป็น double d ผ่านการ assign ด้วยเครื่องหมาย = แบบอัตโนมัติ การ convert type แบบนี้เรียกว่า implicit conversion

1
2
double d = 305.5;
float f = (float) d;

ในทางตรงกันข้าม เราไม่สามารถแปลง double d เป็น float f ได้โดยตรง ต้องใช้ operator cast โดยระบุ float เป็น parameter เราเรียก convert ที่ต้องระบุ type ปลายทางว่า explicit conversion

User-Defined Conversion

เนื่องจาก float และ double เป็น build-in type เราจึงสามารถใช้คุณบัติ implict และ explict conversion ที่มีอยู่

ในกรณีที่ประกาศ type ใหม่ เราสามารถเขียน operator สำหรับ convert type โดยใช้ keyword implicit และ explicit โดย operator ต้องประกาศเป็น public static เสมอ

Implicit

1
2
3
4
5
6
7
class MagicNumber {
public int Number { set; get;}

public static implicit operator MagicNumber(int value) {
return new MagicNumber { Number = value };
}
}

จากตัวอย่าง ในคลาส MagicNumber มีการใช้ keyword implicit operator เพื่อ overload operator ชื่อ MagicNumber ซึ่งรับ int ตัวเดียวเป็น parameter สิ่งที่ได้จากการ overload ชื่อคลาสลักษณะนี้ จะทำให้ MagicNumber มีคุณสมบัติ implicit conversion

1
2
int i = 3;
MagicNumber n = i;

คุณสมบัติที่เพิ่มเข้ามาใหม่ ทำให้ MagicNumber n สามารถ assign ค่าใหม่ที่มี type int ได้โดยตรง

Explicit

1
2
3
public static explicit operator int(MagicNumber value) {
return value.Number;
}

การเขียน explict convertsion ต่างจาก implict convertsion เล็กน้อย คือ เปลี่ยนจาก keyword implicit เป็น explicit เท่านั้น

1
2
MagicNumber n = new MagicNumber { Number = 2 };
int i = (int) n;

Links

Explicit conversion ใน C#

การแปลง type ใน C# มีหลายแบบ

  • Implicit conversion - แปลงแบบอัติโนมัติโดย compiler
  • Explicit conversion (cast) - แปลงโดยใช้ keyword as หรือ ใช้ operator cast
  • User-define conversion - เขียน method สำหรับแปลง type ขึ้นเอง
  • Helper class - ใช้ helper ที่มีอยู่แล้ว เช่น คลาส Convert

Explicit conversion

ใช้ cast

1
2
3
4
object x = ...;
if (x is String) {
var str = (String) x;
}

Cast มักใช้คู่กับ is เพื่อทำ type checking สำหรับกัน error InvalidCastException ในกรณีที่ CLR ไม่สามารถแปลง object ให้เป็น type ที่ต้องการได้

ใช้ as

1
2
3
4
var str = x as string;
if (str != null) {
// Use str
}

การแปลง type โดยใช้ as จะช่วยลดขั้นตอน type checking และไม่มีทาง error เนื่องจาก operator as จะคืนค่า null ในกรณีที่ไม่สามารถแปลง type จะต่างจาก cast ที่จะเกิด error InvalidCastException ข้อจำกัดของ as คือใช้ได้กับ type ที่เป็น Nullable เท่านั้น

ใช้ as ใน for

1
2
3
for (var str = x as string; str != null; str = null) {
// Use str
}

เราสามารถใช้ as ว่างไว้ใน expression for เพื่อกันตังแปร str ให้อยู่ใน scope วิธีนี้เป็น programming trick ที่ทำให้โค้ดอ่านยาก จึงไม่นิยมใช้

Performance

การ convert type มีผลกับประสิทธิภาพของโปรแกรม ถ้าไม่สามารถหลีกเลี่ยงได้ ก็ควรทำให้น้อยที่สุด การใช้ as แทน cast จะช่วยลดจำนวน conversion ได้หนึ่งครั้ง เนื่องจากการทำ type checking ด้วย is จะเกิด type conversion เช่นกัน

อย่างไรก็ตามการใช้ as กับข้อมูลประเภท Nullable ของ value type เช่น int? จะ drop performance ของโปรแกรมมากกว่าการใช้ is + cast อย่างมีนัยยสำคัญ เนื่องจากการแปลง value type ให้เป็น Nullable จะมี operator อื่นเพิ่มเข้ามา

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Test {
const int Size = 30000000;
static void Main() {
object[] values = new object[Size];
for (int i = 0; i < Size - 2; i += 3) {
values[i] = null;
values[i+1] = "";
values[i+2] = 1;
}
FindSumWithCast(values);
FindSumWithAs(values);
FindSumWithLinq(values);
}
static void FindSumWithCast(object[] values) {
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values) {
if (o is int) {
int x = (int) o;
sum += x;
}
}
sw.Stop();
Console.WriteLine("Cast: {0} : {1}", sum, (long) sw.ElapsedMilliseconds);
}
static void FindSumWithAs(object[] values) {
Stopwatch sw = Stopwatch.StartNew();
int sum = 0;
foreach (object o in values) {
int? x = o as int?;
if (x.HasValue) {
sum += x.Value;
}
}
sw.Stop();
Console.WriteLine("As: {0} : {1}", sum, (long) sw.ElapsedMilliseconds);
}
static void FindSumWithLinq(object[] values) {
Stopwatch sw = Stopwatch.StartNew();
int sum = values.OfType<int>().Sum();
sw.Stop();
Console.WriteLine("LINQ: {0} : {1}", sum, (long) sw.ElapsedMilliseconds);
}
}

จากตัวอย่าง FindSumWithCast การ cast แบบปกติ สามารถ unbox object ให้เป็น type int ได้โดยตรง ส่วน FindWithAs จะต้อง unbox ให้เป็น Nullable int หรือ int? ซึ่งจะมี internal operation เกิดขึ้นในฟังก์ชั่น JIT_Unbox_Nullable ของ JIT

1
2
3
Cast: 10000000 : 74
As: 10000000 : 316
LINQ: 10000000 : 913

ผลการรัน ในกรณีที่ใช้ cast ให้ประสิทธิภาพที่ดีกว่าการใช้ as ถึงแม้จะมี type conversion เกิดขึ้นสองครั้งก็ตาม

Links

Fizz Buzz in F#

Fuzz Buzz

Fizz Buzz เป็นโจทย์ที่นิยมใช้สำหรับ ทดสอบการเขียนโปรแกรม กฎคือ ให้เขียนโปรแกรมที่รันเลขตั้งแต่ 1 - 100 โดย

  1. ถ้าเลขนั้นหาร 3 ลงตัวให้พิมพ์ Fizz
  2. ถ้าเลขนั้นหาร 5 ลงตัวให้พิมพ์ Buzz
  3. และถ้าเลขนั้นหารทั้ง 3 และ 5 ลงตัวให้พิมพ์ FizzBuzz

ใน F# สามารถแก้ปัญหานี้ได้หลายวิธี วิธีที่ง่ายสุดคือใช้ Pattern matching อีกวิธีคือใช้ Active pattern ซึ่งจะทำให้โค้ดอ่านง่ายขึ้น ส่วนวิธีสุดท้ายที่ยากสุดคือ ใช้ Computation expression

Pattern matching

Pattern matching แบบแรกคือ ใช้วิธีีคำนวณตัวเลขทีตำแหน่ง matching case โดยใช้ when guard

1
2
3
4
5
6
7
8
9
10
let fizzBuzz number =
match number with
| n when n % 15 = 0 -> "FizzBuzz"
| n when n % 3 = 0 -> "Fizz"
| n when n % 5 = 0 -> "Buzz"
| n -> sprintf "%d" n

[1..100]
|> List.map fizzBuzz
|> List.iter (printfn "%s")

หรือคำนวนในประโยค match ... with

1
2
3
4
5
6
7
let fizzBuzz i =
match i % 3, i % 5 with
| 0, 0 -> "FizzBuzz"
| 0, _ -> "Fizz"
| _, 0 -> "Buzz"
| _ -> string i
[1..100] |> Seq.map fizzBuzz0 |> Seq.iter (printfn "%s")

Partial active pattern.

ใช้ Partial active pattern สองตัว คือ P3 และ P5

1
2
3
4
5
6
7
8
9
10
11
12
let (|P3|_|) i = if i % 3 = 0 then Some i else None
let (|P5|_|) i = if i % 5 = 0 then Some i else None

let f = function
| P3 _ & P5 _ -> printfn "FizzBuzz"
| P3 _ -> printfn "Fizz"
| P5 _ -> printfn "Buzz"
| x -> printfn "%d" x

Seq.iter f {1..100}
//or
for i in 1..100 do f i

หรือใช้ Partial case เดียว คือ DivisibleBy ซึ่งเป็น Optimize version ของแบบแรก

1
2
3
4
5
6
7
8
let (|DivisibleBy|_|) divisor i =
if i % divisor = 0 then Some () else None
for i in 1..100 do
match i with
| DivisibleBy 3 & DivisibleBy 5 -> printfn "FizzBuzz"
| DivisibleBy 3 -> printfn "Fizz"
| DivisibleBy 5 -> printfn "Buzz"
| _ -> printfn "%d" i

Computation expression

วิธีที่ยากสุด คือ เขียนด้วย Computation expression

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type M<'T>  = M of 'T
type MonadBuilder() =
member this.Return x = M x
let m = MonadBuilder()
let fizz = function
| x when x % 3 = 0 -> m { return x, "Fizz"}
| x -> m { return x, x.ToString() }
let buzz = function
| M (x,s) when x % 5 = 0 -> m { return x, s + "Buzz" }
| M (x,"") -> m { return x, x.ToString() }
| M (x,s) -> m { return x, s }

[1..100]
|> List.map(fizz >> buzz)
|> List.iter(fun (M (_,s)) -> printfn "%A" s)

Links