なんでこんなのを書いてるのか
F# を布教1するたびに誤解を解いたりどこらへんが良いのか列挙したりするの疲れたし, URL だけ投げつければ済むようにしたいからです.
F# とは, なぜ F# なのか
F# は書いていて非常に楽しく, かつ何でもできてしまうので, 趣味のプログラミングのお供には非常に最適な言語です. またバグの少ないプログラムを書くための機能が充実していると同時に, コード量が少なく済みメンテナンスしやすい設計になっているので, 実行速度の速さと相まって, 金融系企業や研究機関を中心に, 機械学習研究者やモバイルアプリ開発者などにも高く評価されています.
1. 色々な言語のいいとこ取り
F# は OCaml という言語をベースに開発された2プログラミング言語で,
- C# の速度・クロスプラットフォーム性・ライブラリの多さ・開発環境
- Rust のツールチェイン
- Go のデプロイしやすさ
- Python のオフサイドルール
- Haskell のモナド文化
- 静的ダックタイピング(事実上のトレイト/型クラス)
- コンパイル時型生成(type providers)
といった要素を兼ね備えています. 構文が非常に軽量で, 型推論がよく効くため型アノテーションをほとんど書かなくていいのが特徴です.
例えば Hello, world! はこんな感じです:
printf "Hello, world!"
2. だいたい全部 F# でできる
fsharp.org のガイドを見ていただければわかるとおり, F# では非常に多くのことをすることができます.
- Web 開発: Suave や Giraffe, Saturn などのライブラリを使ってバックエンドを書いて, F# to JS コンパイラである Fable でフロントエンドを書けます3. また Blazor の F# 用サポートライブラリである Bolero を使えば, F# を WebAssembly にコンパイルして Web アプリを作ることもできてしまいます4.
- データサイエンス: Microsoft がホストしている Azure Notebook ではデフォルトで F# を補完付きで使うことができます5. 数値計算用ライブラリ Math.NET Numerics や自動微分ライブラリ DiffSharp などが用意されており, XPlot を使って結果を可視化できます.
- モバイルアプリ開発: Xamarin は当然使うことができますし, Xamarin.Forms を使ったいい感じの UI ライブラリ Fabulous もあります.
- ゲーム開発: XNA の後継である MonoGame や Xamarin 製の Urho3D ラッパー UrhoSharp は普通に使えます. Unity はちょっと厳しめ.
- 機械学習: Microsoft は機械学習用の言語として F# を推しているようで, 公式紹介ページ にもちゃっかり登場しています. ライブラリ面では MS による ML.NET6 や, サードパーティの全部入り系ライブラリ Accord.NET など様々なライブラリが揃っています. さらに,SciSharp STACK により TensorFlow や Keras, Numpy などへのバインディング/完全な移植が提供されています.
- クラウドコンピューティング: F# は Microsoft Azure で公式サポートされており(MS製なのでまあ当然ですが), AWS は .NET 向け SDK/Toolkit を提供しているのでそこでも使うことができます. また AWS Developer Blog に F# の使い方の記事 があります7. また Docker image もあります.
- 処理系開発: もちろん. Lex/Yacc や Parsec など必要なものは揃っています8. スタンドアロンなバイナリで配布できるとかの嬉しさもあります. わたしも自作言語を F# で作ってます.
このようにできることが尋常でなく多いので, なにか違うことをやりたくなっても他の言語を覚える必要があまりなくなります.
3. だいたいどこでも動く
F# は .NET VM 上で動く言語なので, .NET VM が動く環境ならどこでも動かすことができます.
例えば, 処理系の1つ .NET Core の動作環境は ここ, Mono の動作環境は ここ に書いてあります. また, 有名な Xamarin を用いればスマートフォンアプリ開発までできてしまいます.
後述の通り, どの処理系・プラットフォームでも使える API の水準が定められており, この範囲で書いている限りはどの環境でも動作することが保証されています.
よくある誤解・質問
Q. Windows でしか使えないんでしょう?
A. わたしは GNU/Linux でしか F# 書いたことないです
.NET の API にはいくつか種類・水準があります.
-
Windows に乗っているのがいわゆる "フル" の .NET Framework で, ここには WPF (UIツールキットの一種) などの Windows 専用の API も含まれています.
-
*nix 向けの老舗9 .NET 実装である Mono はフルの .NET のうち移植できないものを除いた大部分の API をサポートしています.
- 最近10定義された .NET Standard は, .NET Framework の API のうちプラットフォームによらないポータブルな部分です.
- .NET Core は Microsoft による最新のクロスプラットフォーム .NET 実装+開発環境で, .NET Standard をサポートしています. (超オススメ!!)
.NET Core は Windows / Mac OS X / Linux 用にそれぞれ SDK とランタイムが用意されています.
主要な Linux distro には repo も用意されていて, パッケージマネージャに管理させることもできます. OS X では Homebrew Cask にもパッケージがあるようです. またすべてのプラットフォームで root 権限が要らないバイナリ版を使うこともできます.
またライブラリ類も .NET Framework 標準の API が非常に充実しているだけでなく, C# で書かれた大量のライブラリを利用することができます11.
Windows じゃないと困ることは Visual Studio を使えないことくらいです.
Q. Visual Studio がないと書けないのでは?
A. CUI だけでも書けますし, VSCode も快適です
Vim プラグイン 12や Emacs mode があり, IntelliSense 補完やオンザフライでのシンタックス/コンパイルエラーチェック, 定義されているソースへのジャンプなどを使うことができます.
また, Ionide という VSCode 用の F# 拡張機能があり, こちらでは上に加えて CodeLens での型シグネチャ表示やマウスオーバーでの型表示, GUI でのデバッグなどもすることができます.
なお搭載されている補完エンジン自体はすべて共通のもので, Visual Studio のものよりは賢くないですが十分便利です.
.NET Core はパッケージの追加やアプリケーションの実行などで dotnet
コマンドを多用するので, そこらへんはむしろ *nix のほうが楽まであります.
Q. 型システムが弱いって聞いた
A. そんなことはない
F# にはインライン関数というものがあって, コンパイル時に消えるのをいいことに, その内部では本来の .NET の型システムでは許されないような様々な暴挙を働くことができます.
どんなことができるのかは わたしの ブログ とかみてください. 誤解を恐れずに言えば, Haskell の型クラス13と同等の機能があります.
ブログの記事でやっているようなことを使って Haskell における様々な概念を使えるようにしたライブラリが, 上で挙げた FSharpPlus です. なお F# Foundation 公式プロジェクトの1つです.
Q. VM 言語だし(例えば OCaml や Haskell より)遅いのでは?
これはただのベンチマークなので15そこまで参考になるわけではないですが, 一般に言って .NET/F# にはパフォーマンス上他の処理系より利点となりうる要素がそれなりにあります.
1. boxing を極力排除できる/される
.NET では値型と参照型が区別されており, 前者に対する操作は unboxed なまま行われます. また JVM と異なりユーザが値型を定義して使うことができます(struct
). F# においても, 再帰的でない代数的データ型やレコード型を任意に値型にすることができます.
また .NET はバイトコードレベルで1階の型システムを積んでいて16, 型変数が値型で具体化される際には専用のコードを JIT で生成して余計な boxing/unboxing が発生しないようにします17. これは boxing を使ってパラメータ多相を実現する多くの処理系とは大きく異なる点です. このおかげでハッシュテーブルなどの多相な(ジェネリックな)データ構造はそのような処理系(OCaml, Haskell を含む)と比べてかなり高速です
18.
2. 並列・非同期処理が非常に楽
.NET は async/await や Reactive Extensions といった非同期プログラミング機構の流行の火付け役でもあります. F# ではそれらの機能を簡単に使うことができます.
F# では async/await は async
コンピューテーション式を用いて書かれます. 標準ライブラリに搭載されているので気軽に使うことができます.
let asyncOperation =
async {
let! cmp1 =
heavyComputation1() |> Async.StartChild // 非同期で計算スタート
let! cmp2 =
heavyComputation2() |> Async.StartChild // 同上
do! networkSend "working!"
let! result1 = cmp1 // 結果が出るまで待つ
let! result2 = cmp2 // 同上
return result1 + result2
}
let!
が別の非同期処理の結果を変数に束縛, do!
が結果を返さない(unit
型を返す)非同期処理の実行をそれぞれ行う文で, return はこの非同期処理の結果を返す文です.19
Reactive Extensions も Control.Observable モジュールが標準ライブラリに用意されていますが, FSharp.Control.Reactive パッケージを導入することで observe
コンピューテーション式も使えるようになり, さらに簡単に扱えます.
また他にも C# で使われる Task
や並列計算を行う Parallel.For
, 生の Thread
なども扱うこともできます. 実際冒頭で示したベンチマークでも, 多くのケースで F# はこれらの機構を使って OCaml よりも CPU load を最適化しています.
3. インライン関数がある(F#)
上でも触れましたが, F# では再帰的でない関数に let inline
というキーワードを用いることで, その呼び出しのインライン展開を強制することができます. これはもちろんパフォーマンス上の利点にもなります.
F# のインライン関数はバイナリにコンパイルされてもメタデータとして残っているので, バイナリで配布されているライブラリからでもインライン展開できるのが特徴的です.
Q. C#/F# で書かれたライブラリって F#/C# でそのまま使えるの?
A. F# から C# を呼ぶのは基本的には簡単. 逆は少し大変だができる.
先述の通り .NET はバイトコードレベルで型システムを積んでいるので, C# も F# も VB.NET も型システムは共通になっています. そのため, C#/F# で書かれた型を F#/C# の型システムでそのまま解釈することができます.
つまり, 実際に違いとして出てくるのはそれぞれの言語の標準ライブラリや, 言語機能の実装に使われている型の違いになります.
例えば, C# の次のコード
public class A
{
public static int Add(int x, int y)
{
return x + y;
}
}
は F# では
type A =
class
static member Add : x:int * y:int -> int
end
というシグネチャを持つ型として解釈されます. つまり C# の多引数メソッドは, F# ではタプルを引数に取る関数として扱われるわけです.
この種の便利な変換をしてくれないのは, そもそも使われている型自体が違う場合です. 例えば C# ではメソッドはファーストクラスではなく, ラムダ式は Func<...>
型か Action<...>
型を持つオブジェクトとして扱われます.
一方 F# の関数値は基本的に FSharpFunc<_, _>
型にコンパイルされ, ラムダ式は特別に Action
や Func
を期待される文脈ではそれらの型になり, そうでない場合は FSharpFunc
型になります. そのため, C# の高階メソッドを呼ぶ場合, 関数のまま渡すかラムダ式に包むかで型エラーになったりならなかったりします.
> [1;2;3].ToList().ForEach(fun i -> printfn "%i" i);;
1
2
3
val it : unit = ()
> [1;2;3].ToList().ForEach(printfn "%i");;
[1;2;3].ToList().ForEach(printfn "%i");;
---------------------------------^^^^
/home/user/stdin(11,34): error FS0001: This expression was expected to have type
'System.Action<int>'
but here has type
''a -> unit'
また F# の標準ライブラリにある型を C# から呼ぶ場合, それらは F# 独自の機能(各種リテラルや独自演算子の定義など)を前提に作られているため, 書き方が冗長になってしまう場合があります. 例えば, F# のリストは F# では
let xs = [1;2;3]
と書けますが, これは連結リストとして実装されているため, C# で同じ物体を作ろうとすると
var xs = List<T>.Cons(1, List<T>.Cons(2, List<T>.Cons(3, List<T>.Nil)));
となってしまいます. 一方 C# の標準ライブラリは F# の標準ライブラリでもあるため, F# から C# の物体を扱う場合は C# と同様の記法で済みます.
ただし, C# のライブラリの高階メソッドは F# をはじめとした ML 系言語の機能を模倣するためにあるものが多い(LINQ など)ので, F# にあるものを使ってしまえば済む場合が大半です. 上の例も, 普通なら
[1;2;3] |> List.iter (printfn "%i")
と書きます. よって F# から C# 製のライブラリを使うのに困ることはあまりありません.
F# で C# (.NET) の機能を使う例は F# for fun and profit の Anything C# can do... と Seamless interoperation with .NET libraries で解説されています. というか, C# ユーザの方なら Why use F# シリーズがそもそも C# ユーザ向けに書かれているため, この記事よりそちらを読んでいただくほうが良いかもしれません2021.
開発・実行環境: .NET Core & VSCode
特長
.NET Core は最新の .NET 実装なだけあって, 今までの実装にはない特長が数多くあります.
1. クロスプラットフォーム
- Mono と同じく, .NET Core は Windows, OS X, Linux のどのプラットフォームでも全く同じ開発・実行環境を使うことができます.
- プラットフォーム依存なコードを書かない限り, プラットフォーム依存な問題は発生しません.
- VSCode などのクロスプラットフォームなエディタを使えば, たとえ短期間で異なる OS 環境を行き来するような事態になっても, ソースコードを持ってくるだけでそのまま作業を続行できます22.
2. 高速な動作, 簡単な配布
- .NET Core は現状で最も高速な .NET 実装の1つで, ベンチマーク上では例えば Go とほぼ同等〜少し速い程度のパフォーマンスを発揮します.
- .NET Core は Go と似たようなスタンドアロンバイナリの生成をサポートしており23, .NET Core ランタイムがインストールされていない Windows / OS X / Linux 環境上で実行可能な状態で配布することができます.
3. ツール類が dotnet
コマンドに集約されていて, ほとんどの操作がこれだけで完結する
- .NET Core には
dotnet
という CLI ツール が同梱されており, Rust におけるcargo
コマンドと同様の立ち位置・同等の強力な機能24を備えています. - SDK 自体に同梱されているので何もしなくても使えますし, コンパイラを作っているのと同じところが作っているので余計な互換性問題を考えなくて済むのも利点です(
cargo
と同じように).
インストール
.NET Core SDK は上述の通り, ここ からインストールすることができます. *nix では何らかのパッケージマネージャに乗っかっておいたほうがアップデートが楽ですが, 直接ダウンロードするバイナリは root 権限がなくても使うことができます.
VSCode は ここ からインストールすることができます. 同様にパッケージマネージャに乗っかると楽です.
F# は .NET Core SDK に標準搭載されているので, コンパイルするだけならこれ以上のインストールは必要ありません. VSCode で F# を書くには拡張機能の Ionide が別途必要です25.
なお, エディタは前述の通り補完エンジンが共通なので VSCode でなくてもそこまで変わらないです.
実際に触ってみる
インストールが完了したならば, 早速 F# で Hello, World! してみましょう.
$ dotnet new console -lang="F#" -o helloworld
でテンプレートから helloworld
フォルダに生成されます.
このとき生成される3つの物体について説明しておくべきでしょう:
1. Program.fs
… ソースコードです.
open System // 名前空間やモジュールをオープンするやつです
// [<EntryPoint>] は属性と呼ばれるもので, これが付いた
// 関数がプログラムのエントリーポイントになります.
// 属性さえついていれば名前は `main` でなくてもOKです.
// 実行したいコードは必ずここに書かなければいけないわけではなく,
// この外に `printf` など処理を書いても実行されます.
// ただし, コマンドライン引数を取りたい場合はこの関数で受け取るしかありません.
[<EntryPoint>]
let main argv (* コマンドライン引数 (strint array) *) =
printfn "Hello World from F#!" // 'n' は newline の意
0 // エントリーポイントは return code (int) を返す必要があります
2. helloworld.fsproj
… Rust における Cargo.toml
に相当するもの.
コンパイルするファイルや依存パッケージ, メタデータ類を記述します.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>
</Project>
3. obj/
… コンパイル時のキャッシュなどが入るディレクトリです.
基本的に触る必要はなく, 配布する必要もないので .gitignore
などに入れておくべきです.
さて, コンパイルして実行してみましょう. と言いましたが, 実行するコマンドを打つだけで, 必要な場合26は勝手にコンパイルが走ります.
$ dotnet run
Hello World from F#!
ではパッケージを追加してみましょう. 拙作の FSharp.Scanf ライブラリ27を導入して, フォーマット付きで入力を受け付けられるようにしてみます.
$ dotnet add package FSharp.Scanf
Writing /tmp/tmpmQgFq7.tmp
info : パッケージ 'FSharp.Scanf' の PackageReference をプロジェクト '/home/.../helloworld.fsproj' に追加しています。
log : /home/.../helloworld.fsproj のパッケージを復元しています...
info : GET https://api.nuget.org/v3-flatcontainer/fsharp.scanf/index.json
info : OK https://api.nuget.org/v3-flatcontainer/fsharp.scanf/index.json 691 ミリ秒
info : GET https://api.nuget.org/v3-flatcontainer/fsharp.scanf/2.2.6831.16169/fsharp.scanf.2.2.6831.16169.nupkg
info : OK https://api.nuget.org/v3-flatcontainer/fsharp.scanf/2.2.6831.16169/fsharp.scanf.2.2.6831.16169.nupkg 773 ミリ秒
log : FSharp.Scanf 2.2.6831.16169 をインストールしています。
info : パッケージ 'FSharp.Scanf' は、プロジェクト '/home/.../helloworld.fsproj' のすべての指定されたフレームワークとの互換性があります。
info : ファイル '/home/.../helloworld.fsproj' に追加されたパッケージ 'FSharp.Scanf' バージョン '2.2.6831.16169' の PackageReference。
すると, helloworld.fsproj
が次のように変更されます.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>
<!-- ↓New! -->
<ItemGroup>
<PackageReference Include="FSharp.Scanf" Version="3.0.3291.28624" />
</ItemGroup>
</Project>
では, Program.fs
を書き換えてみましょう.
open FSharp.Scanf
printfn "what is the ultimate answer?"
try
let ans = scanfn "%i"
if ans = 42 then
printfn "correct."
else
printfn "%i? no." ans
with
| _ -> printfn "you entered something other than a number."
わたしは 2 spaces でインデントするのが好きなのでそのように書き換えました28. また今回はコマンドライン引数が要らないのでエントリーポイントは使いません.
では, 実行してみましょう.
$ dotnet run
what is the ultimate answer?
42
correct.
$ dotnet run
what is the ultimate answer?
0
0? no.
$ dotnet run
what is the ultimate answer?
foo
you entered something other than a number.
このように, .NET Core における開発は基本的にはシェルで, dotnet
コマンドを使って行われます. これは Ionide(VSCode) などを使っている場合でも同様です.
ただし, dotnet
コマンドではソースコードの追加・移動・削除をするコマンドがデフォルトでは用意されておらず, .fsproj
を直接編集するか Ionide などのエディタ拡張の機能で行う必要があります.
モジュールに関する注意
F# ではコードが上から順番に解釈される(自分より下に記述した関数を呼び出すことができない)だけでなく, .fsproj
において上から書かれた順にソースファイルを読み込みます.
前者は module rec
を使う ことで解決できますが, 後者はどうにもなりません. ただし, 既存のモジュールと同名のモジュールを別の名前空間に作っても特に問題はないので,
// A.fs
module A
let f x y = x + y
// B.fs
module B
let g x = A.f x 1
// AfterBDefined.fs
[<AutoOpen>] // この属性が付いていると中身が自動でグローバル名前空間に展開されます
module AfterBHasDefiend
module A =
let h x = B.g (x*2)
のようにすれば, 使う側からは A
は f
と h
を持つ単一のモジュールに見えます.
また複数ファイルからなる F# プログラムでは, 上の例のように, 各ソースファイルの先頭に名前空間宣言 (namespace Foo
) もしくはモジュール宣言 (module Foo
) を置かなければなりません. 以後に続くトップレベルの全てが先頭で宣言した名前空間/モジュールの中身に入ることになります. 詳しい解説は MSDN にあります.
余談(追記): 依存関係は良くないし, 相互参照は最悪である
と書いたところ "上から順にしか関数を定義できないのが言語としてブサイク" と反応を頂いた29ので, なんでこういう仕様になっているのかをちょっと書いてみたいと思います.
まず前提として, 循環参照する 値(関数) や 型 は特に問題ではありません. F# でもそれらは書くことができます.
let rec isEven x =
if x = 0 then true else isOdd (x - 1)
and isOdd x =
if x = 0 then false else isEven (x - 1)
type Tree<'a> = TEmpty | TNode of 'a * Forest<'a>
and Forest<'a> = FNil | FCons of Tree<'a> * Forest<'a>
しかしご覧の通り, どの関数同士が・どの型同士が相互再帰しているのかを and
キーワードを使って明示しなければいけません. なぜならば, そうすることで依存関係を 明確かつ最小限に することができるからです.
F# では(というか一般的な ML 系言語は), 今書いている関数の中で使う関数は, それより前に定義されていなければなりません. これによって関数同士の依存関係が非常に明確になり(前を探せば良いのですから), メンテナンスが容易になります. 循環参照する値や型を and
でまとめて近くに置いておけば, この "前を探せばいい" ルールが破れている箇所が最小限になり, 注意しやすくなります.
依存関係があると, ある一箇所を変更したときにそれに依存する箇所が次々と壊れてしまいます. そのため, より一般的・抽象的・変更の可能性が少ないコード を下のレイヤに, より局所的・具体的・変更の可能性が大きいコード を上のレイヤに配置し, それぞれのレイヤはより下位のレイヤにしか依存しないようにすることでメンテナンスを容易にする, というのが昔から知られた手法です.
循環参照する モジュール がこれをぶち壊しにすることは明確でしょう. モジュールはまさにレイヤを表現するもので, それが循環参照したならば "それぞれのレイヤはより下位のレイヤにしか依存しない" という原則自体を破ります. それゆえ F# は(一般に ML 系言語は)循環参照するモジュールを許しません. 一方循環参照する値や関数は, 同じレイヤに属する存在である限り, 制限する理由はありません.
詳しくは F# for fun and profit - Cyclic dependencies are evil を読んでください.
F# の特徴的な言語機能
オフサイドルール
F# は Python や Haskell などと同様にオフサイドルールを採用しており, インデントが構文解析に大きく影響する代わりに, 元となった OCaml より記述量が少なく済むようになっています30.
具体的には, 次のように簡略化されます.
// verbose
let f x =
let a = 1 in
let b = 2 in
x + a + b
in f 42
// lightweight
let f x =
let a = 1
let b = 2
x + a + b
f 42
// verbose
module A = begin
let f x = x + 1
end
// lightweight
module A =
let f x = x + 1
// verbose
if b then begin
printfn "a";
printfn "b";
printfn "c"
end
// lightweight
if b then
printfn "a"
printfn "b"
printfn "c"
// verbose
match a with
| Some b ->
begin match b with
| Pos i -> int i
| Neg i -> -int i
end
| None -> 0
// lightweight
match a with
| Some b ->
match b with
| Pos i -> int i
| Neg i -> -int i
| None -> 0
なお, OCaml と同様の構文も #light "off"
ディレクティヴをファイルの先頭に置いてあげると使えます. この場合はインデントは構文に影響しなくなり, その代わりに様々な場所で in
や end
などが必要になります.
オフサイドルールについては F# syntax: indentation and verbosity - F# for fun and profit に詳しいです.
Computation Expressions
コンピューテーション式(Computation Expressions) は定型的な関数呼び出しに対していい感じな DSL を定義して使うことができる機能です31.
例えば, 九九を列挙する遅延リストも:
seq {
for a in 1..9 do
for b in 1..9 do
let text = sprintf "%i*%i=%i" a b (a*b)
yield (a, b, text)
}
// val it : seq<int * int * string> =
// seq
// [(1, 1, "1*1=1"); (1, 2, "1*2=2"); (1, 3, "1*3=3"); (1, 4, "1*4=4"); ...]
let heavyComputation = async {
do! Async.Sleep 1000
return 42
};;
// val heavyComputation : Async<int>
let exec = async {
let startTime = DateTimeOffset.UtcNow
let! cmp1 = heavyComputation |> Async.StartChild
let! cmp2 = heavyComputation |> Async.StartChild
let! result1 = cmp1
let! result2 = cmp2
do printfn "%fms" (DateTimeOffset.UtcNow - startTime).TotalMilliseconds
return result1 + result2
};;
// val exec : Async<int>
Async.RunSynchronously exec;;
// 1003.727000ms
// val it : int = 84
一見異なる構文のように見えて, 実は同じ Computation Expression の仕組みを使って実装されています.32
Computation Expression はコンパイル時に糖衣が剥がされて, 事前に定義されたルールによって関数呼び出しに変換されます. そしてユーザがそれを定義することで, 比較的容易に Computation Expression を自作することができます.
またデフォルトで用意されているものだけでなくユーザが独自の拡張構文を導入することもできるので,
パッケージ管理システム NuGet の設定ファイルのDSLを作れたり:
let nugetDef =
nuget {
rootDirectory "c:\\dev"
toolsDirectory "c:\\dev\\tools"
outputDirectory "c:\\dev\\output"
packageProject {
id "Foo.Bar"
version (v"1.2.3")
includeDependency !> ("xunit", v"1.9.1", Net40)
includeDependency !> ("autofac", v"1.0.0")
}
}
モナディックにコマンドラインアプリを組めるライブラリ33を作れたりします:
let colorOption =
commandOption {
names ["color"; "c"]; description "Colorize the output."
takes (format "red" |> asConst ConsoleColor.Red)
takes (format "green" |> asConst ConsoleColor.Green)
takes (format "blue" |> asConst ConsoleColor.Blue)
suggests (fun _ -> [CommandSuggestion.Values["red"; "green"; "blue"]])
}
let echoCommand =
command {
name "echo"
displayName "main echo"
description "Echo the input."
opt color in colorOption |> CommandOption.zeroOrExactlyOne
do! Command.failOnUnknownOptions()
let! args = Command.args
do
let s = args |> String.concat " "
match color with
| Some c -> cprintfn c "%s" s
| None -> printfn "%s" s
return 0
}
Computation Expression の仕組み・作り方についてはここが詳しいです.
また, FSharpPlus には 任意のモナドに使える do-notation もあります.
inline
関数, Statically Resolved Type Parameters
インライン関数 (inline functions) はその名の通りコンパイル時にインライン化される関数です.
let inline addTwice x y = x + y + y
addTwice 2 3 // `2 + 3 + 3` になる
インライン関数自体はそれだけなのですが, 前述の通り "コンパイル時に消える" という性質を活かし, .NET では許されないような様々な型システム拡張を実現するのに使われます.
前提として, F# においては任意の型にメンバ変数/関数を追加することができます34.
type Foo = FooInt of int | FooStr of string
with
member this.str =
match this with
| FooInt i -> sprintf "%i" i
| FooStr s -> s
static member isInt x =
match x with
| FooInt _ -> true
| FooStr _ -> false
printfn "%s" (FooInt 42).str // "42"
printfn "%s" (FooStr "bar").str // "bar"
Foo.isInt (FooStr "bar") |> printfn "%b" // false
そして, インライン関数内では, 型パラメータにおいてその型がある特定のメンバを持っていることを要求することができます.
let inline getStr (x: ^X) =
(^X: (member str: string) x)
// val inline getStr : x: ^X -> string
// when ^X : (member get_str : ^X -> string)
type 'a Bar = { bar: 'a }
with
member this.str = sprintf "bar: %A" this.bar
FooInt 42 |> getStr |> printfn "%s" // "42"
{ bar=42 } |> getStr |> printfn "%s" // "bar: 42"
このように, コンパイル時に解決される型変数を Statically Resolved Type Parameters (SRTP) と呼びます. SRTP は '
ではなく ^
が頭に付き, メンバ制約(member constraint)を加えることができます.
勘の良い方は既にお気づきの通り, SRTP のメンバ制約は型クラスと同じ働きをします.
例えば, F# のほとんど全ての組み込み演算子35はインライン関数として定義されています36 :
let inline (+) (x: ^T) (y: ^U) : ^V =
(static member (+) : ^T * ^U -> ^V) (x,y))
よって型ごとに演算子をオーバーロードすることができて, しかも(実行時ではなく)コンパイル時に該当の実装に置き換えられます:
type Baz = { bazInt: int; bazStr: string }
with
static member inline (+) (x: Baz, y: Baz) =
{ bazInt = x.bazInt + y.bazInt;
bazStr = sprintf "%s+%s" x.bazStr y.bazStr }
{ bazInt=1; bazStr="a" } + { bazInt=2; bazStr="b" } |> printfn "%A"
// { bazInt = 3; bazStr = "a+b";}
この例では実装もインライン化してあるので, コンパイル時には単にレコードを生成するコードになります.
そして, inline 関数の SRTP だけでなく, データ型の型変数もメンバ制約を持つことができます37.
type Hoge< 'a when 'a: (member Piyo: string) > = ...
なお, F# では等値判定 (equality
) と大小比較 (comparison
) の実装を要求する制約は特別扱いされており18, ^X when ^X: (static member (=): ... )
ではなく ^X when ^X: equality
のように書きます38.
よって, 集合を表すデータ型 Set
には次のような制約が付いています39.
type Set< 'a when 'a: comparison > = ...
インライン関数と SRTP によるアドホック多相機能を存分に活かすことで, 上で何度も紹介している FSharpPlus や わたしのブログ記事 が実現されています.
MSDN に インライン関数, SRTP, 型変数に加えられる制約, 演算子のオーバーロード についてのドキュメントがあります.
Type Providers
型プロバイダ (Type Providers) は, コンパイル時にわかる情報から自動的に型を生成する仕組みです.
メタプログラミングとしてはありがちな(ほんとか?)仕組みですが, 言語自体がサポートしているのが良い点です.
例えば JSON を返す URI を FSharp.Data の JsonProvider に渡すと, バックグラウンドで動作しているコンパイラサービスの働きにより, IDE 上でリアルタイムで補完が効き始めます!
- JSON や XML などをサンプルデータから型を生成して扱える FSharp.Data
- Azure Storage 上のアセットを補完付きで様々に操作できる Azure Storage Type Provider
- SQL データベースアクセスを型安全に行える SQLProvider
- OpenAPI 2.0 に対して API ラッパーライブラリを自動生成できる SwaggerProvider
40 とその後継の OpenAPI Type Provider - ローカルの R 言語の環境に入っているパッケージを読み取ってラッパーを生成する R Type Provider
など, 様々なパッケージが存在します.
実装はあまり進んでいないようですが, 代数的データ型やレコード型の生成 や 型の情報からの型生成 も approved in principle となっており, 将来的にサポートされる可能性があります.
Type Provider の作り方のドキュメントは MSDN にあります.
その他細かな機能
Code Quotation
F# は組み込みでコードをクォートする機能を持っていて, F# の構文木に変換されます.
<@ let f x = x + 10 in f 32 @>
// val it : Quotations.Expr<int> =
// Let (f, Lambda (x, Call (None, op_Addition, [x, Value (10)])),
// Application (f, Value (32)))
これによって F# のコードの生成や, F# のコードから他の言語のコードの生成が実装しやすくなっています.
MSDN にドキュメントがあります. また構文木のコンパイル・実行には FSharp.Quotations.Evaluator やサードパーティの QuotationCompiler などを使うことができます.
Units of Measure
F# では数値型を修飾する(物理)単位を定義し, 使うことができます.
[<Measure>] type kg
[<Measure>] type m
[<Measure>] type s
[<Measure>] type N = kg * m * s^-2
let weight = 50.0<kg>
// val weight : float<kg> = 50.0
let acc = 9.8<m s^-2>
// val acc : float<m/s ^ 2> = 9.8
let power : float<N> = weight * acc
// val power : float<N> = 490.0
また, 単位の換算方法を定義しておけば, 物理量の次元チェックを自動的に行うことができます. 例えば, 拙作の 国際単位系ライブラリ では, mL
と _milli L
と cm^3
が同じ単位であることを認識することができ, SI接頭辞の換算を安全に行うことができます.
let a_1mL = 1.0<_milli L>
let b_1mL = 1.0<cm^3>
let c_1mL = 0.001<L>
let compareML (a: float<mL>) (b: float<mL>) =
printfn "%AmL %s %AmL" a (if a = b then "=" else "<>") b
compareML a_1mL b_1mL
// compareML a_1mL c_1mL (* compilation error *)
let f = 5.0<N>
let m = 4.0<g>
let printAcc (a: float<m/s^2>) =
printfn "%AN = %Ag * %Am/s^2" f m a
printAcc (f / (kilo * m))
// printAcc (f / m) (* compilation error *)
Units of Measure はコンパイル時に消去されるので, オーバーヘッドは発生しません.
Mutable Variables
F# には ref
型(ML 系言語で一般的な抽象化されたポインタ)とは別に, 変更可能な変数 (Mutable Variables) があります.
let x = ref 10 // ref による再代入可能な変数
x := 42 // ref の値の変更
printfn "%d" !x // ref の値の読み出し
let mutable y = 10 // mutable による再代入可能な変数
y <- 42 // mutable 変数の値の変更
printfn "%d" y // mutable 変数の値の読み出し
このように ref
を用いた場合は値への参照を変数に束縛して扱う形になるのに対して, let mutable
で宣言した mutable variables は <-
演算子で値を変更できる以外は通常の変数と同じです.
また ref
では変数がヒープに保存されるのに対し, let mutable
ではスタックに確保されるので, パフォーマンス上多少有利です. ただしスタックの値は定義されたスコープを抜けると消えてしまうため, let mutable
がクロージャにキャプチャされる可能性があるときは, コンパイル時に自動的に ref
に置き換えられてしまいます. 例えば次のような場合です:
let newIncrCounter () =
let mutable i = 0
fun () ->
i <- i + 1
i
// val newIncrCounter : unit -> (unit -> int)
また, ref
では同じ領域を参照する2つの変数を作ることができるのに対して, let mutable
ではそれができません41.
let a = ref 5 // ヒープに新しい領域を確保
let b = a // bは同じ領域を指す
b := 10 // aの中身も同時に変更される
let mutable a = 5 // スタックに値を確保
let mutable b = a // b はスタックに現在の a の値を確保する
b <- 10 // b の中身だけが変わる
さらに, レコード型のフィールドも mutable
キーワードを用いて変更可能にすることができます.
type SampleRecord = {
field1: int
mutable field2: int
}
let sr = { field1=0; field2=1 }
sr.field2 <- 42
sr.field1 <- 42 // コンパイルエラー!
実は, ref
は mutable レコードで実装されています:
// https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/prim-types.fs
type Ref<'T> =
{ mutable contents: 'T }
and 'T ref = Ref<'T>
let ref value = { contents = value }
let (:=) cell value = cell.contents <- value
let (!) cell = cell.contents
Mutable variables と reference cells については Wikibooks の F# Programming/Mutable Data もご覧ください(少し古く, let mutable
がスコープを抜けるとコンパイルエラーになっていた時代のものですが).
Byrefs, byref
-like Structs
Byrefs
Byrefs は関数の引数のみに使うことができる ref
型の変種で, 次の3種があります.
-
't inref
... 型't
の値を読み取ることだけができるポインタ -
't outref
... 型't
の値を書き込むことだけができるポインタ -
't byref
... 型't
の値を読み書きできるポインタ
これにより, ポインタを受け取る際に細かなアクセス制御をかけることができます.
そして, Byrefs を引数に取る関数に実際に渡すことができるのは次の2つです.
-
't ref
型の値 - 変数
v
があるとき, そのポインタ&v
2 に関しては, let v = 10
のように再代入不可の変数として宣言された場合は inref
としてのみポインタを取ることができ, let mutable v = 10
のように再代入可能な変数として宣言された場合は outref
や byref
としてもポインタを取ることができます. また, 変数のポインタは変数が定義されたスコープから出ることができません.
byref
-like Structs
byref
-like structs とは, スタックに確保される値型です. byref
-like structs は使用可能な場所が限られていたり42, クロージャでキャプチャできなかったりなどの制限がある代わりに, 高いパフォーマンスを要求される処理に極めて有効です.
byref
-like structs の例としては Span<'T>
型や Memory<'T>
型があります.
Span<'T>
型 は配列や文字列, NativePtr.stackalloc
関数 でスタックに確保した領域や Marshal.AllocHGlobal
関数 で .NET のGCの管理外メモリに確保した領域などを, その全体でも一部分でも, 包括的かつ効率的に扱うことができる型です.
// https://github.com/fsharp/fslang-design/blob/69c7c47931f8205c1cdf28d5819d675de734bd8e/FSharp-4.5/FS-1053-span.md
let SafeSum (bytes: Span<byte>) =
let mutable sum = 0
for i in 0 .. bytes.Length - 1 do
sum <- sum + int bytes.[i]
sum
let TestSafeSum() =
// managed memory
let arrayMemory = Array.zeroCreate<byte>(100)
let arraySpan = new Span<byte>(arrayMemory);
SafeSum(arraySpan)|> printfn "res = %d"
// native memory
let nativeMemory = Marshal.AllocHGlobal(100);
let nativeSpan = new Span<byte>(nativeMemory.ToPointer(), 100);
SafeSum(nativeSpan)|> printfn "res = %d"
Marshal.FreeHGlobal(nativeMemory);
// stack memory
let mem = NativePtr.stackalloc<byte>(100)
let mem2 = mem |> NativePtr.toVoidPtr
let stackSpan = Span<byte>(mem2, 100)
SafeSum(stackSpan) |> printfn "res = %d"
Memory<'T>
型 は, Span<'T>
型の値がスタック上にしか存在できず boxing できなかったりクロージャでキャプチャできなかったり43して不便なので, 少しパフォーマンスを犠牲にしてそれらを可能にするための型です.
Byrefs と byref
-like structs について詳しくは MSDN のドキュメント, Span<'T>
の嬉しみは ufcpp さんのブログ記事(C#での解説) などを参照してください.
P/Invoke (FFI)
.NET は P/Invoke というネイティヴライブラリとの FFI 機構を搭載していて, F# からも使うことができます.
例えば, libc の getpid
を呼びたい場合は次のように書きます.
open System.Runtime.InteropServices
[<DllImport("libc")>]
extern int getpid()
// 普通の関数のように使う
getpid() |> printfn "%i"
これは F# の機能ではなく .NET の機能なので, 詳しくは MSDN を参照 するなどしてください.
Active Patterns
アクティブパターン(Active Patterns) はパターンマッチで自作のパターンを定義することができる機能です. スマートコンストラクタ(実行時チェックの)と対比して "スマートデストラクタ" とでも言うべき機能です.
Active patterns は定義したいケース名を (|Name1|Name2|...|NameN|)
のように並べたものを変数名とした関数として定義され, その最後の引数でパターンマッチ時の分解対象を受け取ります. そして, それらのケース名を代数的データ型のコンストラクタようにみなして, それで作った値を返すことでケースの場合分けを行います.
これは実例を見たほうが分かりやすいので簡単なものを書いてみます. 例えば
let (|S|Z|) i =
if i = 0u then Z else S (i-1u)
と書けば,
match 42u with
| Z -> printfn "zero"
| S n -> printfn "%i + 1" n
のようにパターンマッチ内で自作のケースを使うことができるようになります. なお, 網羅性検査もしてくれます. 加えて,
type nat = uint32
let S n = n + 1u
let Z = 0u
と型エイリアスとスマートコンストラクタを定義してあげれば, ただの unsigned int を
type nat = S of nat | Z
のようなデータ型と全く同じように扱うことができます.
let rec fact = function
| Z -> S Z
| S n -> S n * fact n
// val fact : _arg1:uint32 -> uint32
また, 網羅性検査ができなくなる代わりに, マッチすることもしないこともあるケースを定義できる partial active patterns もあります. この場合, 戻り値は option
に包まれた形になります.
let (|Fizz|_|) n = if n % 3 = 0 then Some Fizz else None
let (|Buzz|_|) n = if n % 5 = 0 then Some Buzz else None
let fizzbuzz = function
| Fizz & Buzz -> "fizzbuzz"
| Fizz -> "fizz" | Buzz -> "buzz"
| x -> sprintf "%i" x
引数を増やすと, パターンマッチの分解対象以外にもパラメータを受け取ることができます(parameterized active patterns).
let (|Mod|_|) n m = if m % n = 0 then Some Mod else None
let fizzbuzz2 = function
| Mod 3 & Mod 5 -> "fizzbuzz"
| Mod 3 -> "fizz" | Mod 5 -> "buzz"
| n -> sprintf "%i" n
パターンマッチの分解対象は最後の変数で受け取ることに注意してください. またパターンマッチ内で使うときは active patterns の定義と同様にパラメータを先に渡し, 最後の引数に相当する場所で分解結果を束縛します. このため普通の代数的データ型に対するマッチからは逸脱した構文になります(パラメータの部分は curried form になる).
let (|Log|) a x = System.Math.Log(a, x)
match 16.0 with Log 2.0 x -> printfn "log_2 16 = %g" x
// log_2 16 = 4
アクティブパターンも通常の代数的データ型のデコンストラクタと同様に, パターンマッチ以外に関数の引数や let 束縛でも使うことができます.
let (Log 2.0 x) = 16.0
// val x : float = 4.0
let (|DefaultValue|) defVal xo =
match xo with Some x -> x | None -> defVal
// val inline ( |DefaultValue| ) : defVal:'a -> xo:'a option -> 'a
let f (DefaultValue 0 x) (DefaultValue 0 y) = x + y
// val f : int option -> int option -> int
f (Some 4) None |> printfn "%i" // 4
Further Reading
F# for fun and profit
この記事中でも何個かリンクを貼っていますが, F# の機能を1つ1つ紹介するシリーズやその他様々な話題を集めた内容が濃いサイトです.
チュートリアル的なシリーズ記事はどちらかというと既に C# などのオブジェクト指向メインの言語を知っていて F# をやってみたい人向けに書かれています.
しかし, property-based testing の記事など, 既に ML 系言語に慣れている人向けの話題もあります.
特に, Troubleshooting F# は必見です! F# でよくやってしまう間違いとその対処法が網羅されています.
Wikibooks/F# Programming
F# for fun and profit よりはお硬い感じですが, 内容の充実度とわかりやすい実行例でわたしはお気に入りのサイトです.
わたしは MSDN を見る元気がないときにリファレンスとしてよく使います.
MSDN
言わずと知れた公式ドキュメントです. MSDN なので44英語版前提で話を進めます.
MSDN は歩き方を知っていないと, どこ見れば欲しい情報が乗ってるのかわからなく迷子になりがちなので45, 軽く解説します.
-
F# Language Reference ... F# の構文や言語仕様自体に関する解説
-
F# style guide ... コード整形やよく使う手法などを集めたガイドライン
-
F# Core Library Reference ... F# の標準ライブラリのリファレンス
-
Microsoft.FSharp.Core ... 組み込み型や組み込み関数など, 何も open してないのに最初から使えるものはだいたいここに入っている
-
Core.Operators, Core.ExtraTopLevelOperators ... デフォルトで定義済の演算子が見られる(実際に演算子が定義されているモジュールのリファレンス)
-
ヴァーチャル F# エヴァンジェリストになりたいので雇ってください. 藍沢家という前例があるので↩ -
OCaml にあって F# にないものはファンクタとGADTが挙げられます. ファンクタはインターフェースや型クラス的なものがあるためにほとんど必要ないですが, GADT は代替になるものがないので少し困ることもあります. ↩
-
ASP.NET Core の上で実装されており, 仕組み上 .NET Framework のほぼ全機能(=既存のライブラリのほぼ全て)を使うことができるので, まだベータであることを差し引いてもぶっちゃけ Fable よりオススメです. ↩
-
ML(言語)と紛らわしい…… ↩
-
AWS Lambda は .NET Core を使っていますが, cold start が結構遅いため使いどころには気をつける必要があります. これは tiered compilation で多少改善されると思われていましたがそうでもないようです. この問題は ここ で議論されていて, 現行バージョンで改善があまりなかったので ここ で reopen されました. ↩
-
さすがに Menhir はないけど…… だれかポートしてくれないかな ↩
-
2004年にv1.0が出て現在はv5.14が最新です. わたしは 2010 年ごろから使っています. ↩
-
v1.0は2016年で現在はv2.1です. え, PCL? 悲しい事件だったね…… ↩
-
そもそも C# と F# は同じ .NET の型システムを使っているので, どちらからどちらで書かれたライブラリでも使うことができます. 需要が大きいライブラリはまず .NET Standard に対応しているため, 本当に Windows でしか動かないようなもの以外はほぼ全て利用可能です. ↩
-
ちなみにメンテナは私です. 何かあったら気軽に Issue など投げてください. ↩
-
"GHCの" ではない: Orphan instances が完全に許されない. でももしかしたらそのうちできるようになるかも…… ↩
-
CoreRT という .NET Core の AOT コンパイラがありますが, 未だ開発途上なため F# コードの実行にはまだ少なくない制限があります ↩
-
かなり有名なサイトではある ↩
-
まぁこれが F# の型システムを容易には拡張できない足枷にもなっているのですが ↩
-
詳しくは Andrew Kennedy and Don Syme. The design and implementation of generics for the .NET Common Language Runtime を参照. ↩
-
.NET との兼ね合いなど様々な事情により, F# の標準ライブラリの
Map
,Set
などを含む F#の 等値判定と大小比較を使うコードではインライン化 (F#レベル/ILレベル) の恩恵を受けることができず, ユーザ作成の代数的データ型やレコードに用いたときに不必要な boxing/unboxing が発生する ことがある (事情が複雑過ぎてわたしには把握しきれない). これはユーザレベルでならworkaround が存在し, 包括的には F# vNext で改善される見込み. C# の標準ライブラリのDictionary
などを使えば, 現行バージョンでもこの問題は起きない(はず). ↩ ↩2 -
モナドで通じる人向け: bind と return です. ↩
-
最近 F# for fun and profit を和訳しろという圧力を感じてます…… や, そうした方が良いんですが, わたしが全部やるのは単純に身体が1つでは足らないです. ↩
-
この記事はどちらかといえば .NET を知らない・使ったことがない人向けに書いているつもりです. ↩
-
実体験です ↩
-
既定では Go のように単一の実行ファイルが出力されるのではなく, 必要なファイルが全て入ったディレクトリを出力するようになっていますが, .NET Core 3.0 から単一の実行ファイルも作れるようになりました. ↩
-
テンプレートからの新規作成, ビルド, 実行, テスト実行, パッケージのインストール・作成・アップロード, ユーザによるプラグイン機能, npm でのような CLI ツールのインストール etc. ↩
-
VSCode で書くには別途言語サポート拡張機能が必要なのは C# など他の言語でも同じですが ↩
-
まだコンパイルしたことがない, ソースコードが変更された, etc. ↩
-
F# には
printf
はあるが, 入力に関しては文字や文字列単位で読み込むもの(System.Console.ReadLine()
など)のみがあり, 組み込みではscanf
のような関数がない. 拙作の scanf は型安全で,Scanf.Optimized
モジュール下の関数はマイクロ秒オーダーで入力を処理できる(ようにした, 以前と比べ約10倍の高速化. 2019/01/06). ↩ -
タブインデントは言語仕様レベルで禁止されています. ↩
-
お前の bio に書いてある Haskell っていう言語も 基本的には循環参照するモジュールを書けない んだが本当に書いたことあるのか??↩ -
実はわたしはそんなに好きじゃないです. インデントが極端に深くなる書き方を避けようとして, 直し方がわからないエラーに悩まされるので. ↩
-
ただの do-notation だと思われるかも知れませんが, 実際は独自構文を定義できたりする関係でより広い範囲の抽象 (MonadPlus, そもそもモナディックでもなんでもない物体, etc) を扱うことができます. 言い方を変えれば, モナドは computation expressions で扱える抽象の1つに過ぎません. 参考: The F# Computation Expression Zoo ↩
-
遅延リスト(
seq
,IEnumerable<_>
)はコンパイラによって特別扱いされてステートマシンに展開するなどの最適化が入ることもある ↩ -
拙作 ↩
-
クラスベースOOPに馴染みがないならば, 型に密結合したモジュールと考えてください.
static member
はlet
束縛のようなものでモジュールのように型名.名前
で呼び出すことができ,member this.hoge
は値.名前
で呼び出すことができます. ↩ -
!
など特定の型にしか使えないものもあるが, ユーザがグローバル名前空間でインライン関数として再定義すれば, 他の型にも使えるようにできる. ↩ -
実際は特定の組み込み型に対してはコンパイラが直接最適な実装に置き換えるので, そのまま以下のように実装されているわけではない. ↩
-
この際の型変数のプレフィックスは
'
だが, SRTP と同じ扱いになる. ↩ -
代数的データ型とレコードについては, 関数型などの比較不能な型を含んでいない限りは, 自動で
comparison
とequality
が実装される. ↩ -
ちなみに, F# ではモジュールは型ではないため, 型変数を取ってメンバ制約をかけたり, メンバ制約の対象にすることができないのですが, .NET の本来のクラスベースOOの機能を使って, "open できないし内部で型を定義できない代わりに型扱いになるモジュールのようなもの" を作ることができます. これを使えば OCaml のファンクタも一応模倣可能ですが(型変数でモジュールもどきを要求すれば良い), 徹底的にやってる人はわたしは見たことがありません. FSharp.Compatibility.OCaml では型レベルではなく値レベルで模倣しています. ↩
-
知らないうちに死んでた… ↩
-
出典: https://stackoverflow.com/questions/3221200/f-let-mutable-vs-ref ↩
-
ライフタイムが有限かつ静的に決まらないといけない. つまり関数の引数やローカル変数には使えるが, クラスのメンバ変数などには使えない. ↩
-
これは computation expressions の内部で使うことも含む ↩
-
一部は "Microsoft Docs" であって "MSDN" ではないのですが, MSDN ですね. どこがとはいいませんが. ↩
-
docs.microsoft.com と msdn.microsoft.com に分裂しててリンクの貼られ具合によって行ったり来たりする. そのたびにページのレイアウトが変わるため迷子になりやすい ↩