F#
melangの各noteで他言語と比較した、F#の設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。
型
OCaml譲りの強力な型推論を持ち、型注釈はほぼ不要。
let age : int = 20let age : int = 20
let name = "Alice" // 型注釈がなくても型推論で string と判定されるOCamlと同系統のHindley-Milner型推論を持ち、型注釈はほとんどの場面で省略できる。判別共用体取りうる形が閉じた集合になっている型を、caseごとに異なるデータ(payload)を持たせて表現する仕組み。代数的データ型(ADT)のSum Typeに相当し、F#やOCaml(variant type)で使われる呼び方。パターンマッチと組み合わせて使うことで、全caseの網羅をコンパイラに検証させられる。(Discriminated Union)とパターンマッチが標準機能として組み込まれており、`Option<T>`でNullの有無を型として表現する。.NET上で動くため、C#など他の.NET言語ともシームレスに相互運用できる。
型注釈の位置
OCamlと同じpostfix構文・強い型推論を持ち、シグネチャを別ファイルに分離することもできる。
let age : int = 20let age : int = 20
let name = "Alice" // 型注釈なしでも推論される
let greet (name : string) : string = "Hello, " + name
// 型注釈をすべて省略しても推論できる
let greet name = "Hello, " + name変数・引数・戻り値のいずれも`: Type`という後置構文で、OCamlと同じくHindley-Milner推論により実務上はほとんど省略される。`.fs`(実装)とは別に`.fsi`(シグネチャ)ファイルへ型シグネチャだけを分離して書くこともでき、OCamlの`.mli`をそのまま.NET向けに受け継いだ設計になっている。
型変換
組み込みの変換演算子で変換し、縮小変換は検出しない。.NETのTryParseはタプルとして使える。
let y : int64 = int64 xlet x : int32 = 10
let y : int64 = int64 x // 組み込みの変換演算子
let big : int64 = 3_000_000_000L
let z : int32 = int32 big // 黙って切り捨てられる
let ok, n = System.Int32.TryParse("42")
// F#がout引数をタプルの戻り値に変換して見せるOCamlの`Int64.of_int`のような個別関数ではなく、`int`, `int64`, `float`のような組み込みの変換演算子を持ち、C言語風のキャストに近い書き味になっている。数値の縮小変換はC++やJavaと同様に黙って値を切り捨てる。.NET標準の`TryParse`(`bool`と`out`引数)は、F#コンパイラソースコードを解析し、実行可能な形式(機械語や中間表現)に変換するプログラム。静的型付け言語では、実行前にコンパイラが型の不整合などをチェックする役割も担う。が`out`引数を戻り値のタプルへ自動変換して見せるため、C#より扱いやすい形でパターンマッチできる。
暗黙の型変換
OCamlと同様、数値型間の暗黙変換を一切持たない。
let x : int = 10let x : int = 10
let y : float = x // コンパイルエラー
let y : float = float x // OKOCamlを受け継ぎ、intからfloatのような一方向の変換であっても暗黙には行われない。ただし`+.`のような別演算子は持たず、`+`は数値の型を静的に確定できれば`int`・`float`のどちらにも使える(演算子オーバーロードによる静的多相)ため、演算子自体を使い分ける必要はOCamlより薄い。変換には`float x`, `int64 x`のような組み込みの変換演算子を使う。
Enum / Sum Type
判別共用体取りうる形が閉じた集合になっている型を、caseごとに異なるデータ(payload)を持たせて表現する仕組み。代数的データ型(ADT)のSum Typeに相当し、F#やOCaml(variant type)で使われる呼び方。パターンマッチと組み合わせて使うことで、全caseの網羅をコンパイラに検証させられる。が標準機能としてpayloadを持ち、melangが目指すモデルに近い。
type Result = Success of User | Failure of stringtype Result =
| Success of User
| Failure of string
let describe = function
| Success user -> $"ok: {user}"
| Failure message -> $"error: {message}"
// パターン漏れはコンパイラ警告(incomplete match)になるOCaml由来の判別共用体取りうる形が閉じた集合になっている型を、caseごとに異なるデータ(payload)を持たせて表現する仕組み。代数的データ型(ADT)のSum Typeに相当し、F#やOCaml(variant type)で使われる呼び方。パターンマッチと組み合わせて使うことで、全caseの網羅をコンパイラに検証させられる。(Discriminated Union)が標準機能で、caseごとに異なるデータを持てる。パターンマッチの網羅漏れはデフォルトで警告になり、`Option<T>`もこの仕組みで表現される。「不正な状態を表現できなくする」設計をそのまま実現できる型として、書籍『Domain Modeling Made Functional』でも中心的に扱われる。
値の不変性
既定は不変で、変更するときは`mutable`を明示する。OCamlのref型も使える。
let x = ... / let mutable x = ... / mutable fieldtype User = { Name : string; mutable Age : int }
let a = { Name = "alice"; Age = 20 }
let b = { a with Name = "bob" } // 新しい値。aは変わらない
// a.Name <- "x" // コンパイルエラー: mutableではない
a.Age <- 21 // OK: mutableフィールド
let mutable n = 0
n <- n + 1 // OK: mutableな変数`let`束縛とレコードは既定で不変で、`with`で新しい値を作れる。変更したいフィールド・変数には`mutable`を明示的に付ける必要がある。OCaml由来の`ref`型(可変セル)も引き続き使え、.NETとの相互運用のためclassの可変プロパティも扱える。関数型が中心の言語だが、変更を明示して選べる点はOCamlと同じ。
名前付き引数
.NETのメンバーメソッドでは名前付き引数が使えるが、通常のlet関数では使えない。
member this.Foo(width, height) = width + heighttype Calc() =
member this.Foo(width, height) = width + height
let c = Calc()
c.Foo(width = 10, height = 20) // メンバーメソッドは名前付き引数を使える
c.Foo(10, height = 20) // 位置引数と併用可
let foo width height = width + height
foo 10 20 // let関数はカリー化された位置引数のみ。名前付き引数は使えないC#との相互運用を意識した.NETのメンバーメソッド(`member`)呼び出しでは名前付き引数が使え、位置引数と併用したり順序を入れ替えたりもできる。一方、OCaml由来のカリー化された`let`関数では、ラベル付き引数(OCamlの`~label`)に相当する機能自体がなく、位置引数のみになる。同じ言語の中で関数の定義方法によって名前付き引数の可否が変わる、やや非対称な設計。
関数の引数
OCamlと同じくref型で可変性を表すが、.NET由来のbyref引数も併用できる。
let f (r : int ref) = r := !r + 1let f x = x + 1 // 値渡し。xは不変
let incrRef (r : int ref) = r.Value <- r.Value + 1
let c = ref 0
incrRef c // 呼び出し側は普通の値渡しに見える
let incrByref (x : byref<int>) = x <- x + 1
let mutable n = 0
incrByref &n // .NET由来のbyrefは呼び出し側に&が必要OCamlの`ref`型をそのまま受け継ぎ、書き換えたいときはref型(可変セル)の値を渡す設計が基本。加えて.NETとの相互運用のため`byref<T>`引数も使え、こちらはC#の`ref`同様、呼び出し側にも`&`を書く必要がある。同じ言語の中に「印がない可変性(ref型)」と「印がある可変性(byref)」が両方存在する点が特徴。
if 文/式
`if`は式。`else`を省略できるのは、then側が`unit`のときだけ。
if cond then a else blet x = if cond then 1 else 2
// elseを省略できるのはthen側がunitのとき
if cond then printfn "yes"OCamlと同じく、`if`は式で、`else`を省略できるのはthen側が`unit`のときだけである。型によって「値を返す`if`は`else`が必須」が保証される。
I/O の明示
OCamlと同様、通常の関数はI/Oを区別しない。非同期処理も専用のcomputation expressionにとどまる。
let readFile (path : string) : stringlet readFile (path : string) : string =
System.IO.File.ReadAllText(path)
let square x =
readFile "x" |> ignore // 呼べてしまう。コンパイラは止めない
x * xOCamlと同じく、I/Oを行う関数と行わない関数は戻り値の型からは区別されない。非同期処理には`async { }`というcomputation expressionが用意されているが、これは`Async<'T>`という値を組み立てる糖衣構文であり、Haskellの`IO`のように型システムがI/O全体を強制的に分離する仕組みではない。同期的なファイルI/Oなどは、通常の関数呼び出しと同じ構文で書ける。
言語名
"Functional"の頭文字とC#由来の"#"を組み合わせた命名で、記号ゆえの表記揺れも継承した。
Microsoft Researchで開発され、C#と同じ.NET上で動くことを示すために、C#の"#"という命名パターンをそのまま踏襲し、関数型(Functional)であることを表す"F"と組み合わせた。前身であるOCamlからの影響は名前からは読み取れない。C++・C#と同様に`#`はファイル名や多くの文脈でそのまま使えず、`fsharp`という表記に置き換える必要がある。