F#
.NETCore

F# を知ってほしい

なんでこんなのを書いてるのか

F# を布教1するたびに誤解を解いたりどこらへんが良いのか列挙したりするの疲れたし, URL だけ投げつければ済むようにしたいからです.

F# とは

OCaml に

を足して, GADT とファンクタを引いたような言語です.

よくある誤解

Q. Windows でしか使えないんでしょう?

A. わたしは GNU/Linux でしか F# 書いたことないです

.NET の API にはいくつか種類・水準があります.

  1. Windows に乗っているのがいわゆる "フル" の .NET Framework で, ここには WPF (UIツールキットの一種) などの Windows 専用の API も含まれています.

  2. *nix 向けの老舗2 .NET 実装である Mono はフルの .NET のうち移植できないものを除いた大部分の API をサポートしています.

  3. 最近3定義された .NET Standard は, .NET Framework の API のうちプラットフォームによらないポータブルな部分です.

  4. .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# で書かれた大量のライブラリを利用することができます4.

Windows じゃないと困ることは Visual Studio を使えないことくらいです.

Q. Visual Studio がないと書けないのでは?

A. CUI だけでも書けますし, VSCode も快適です

Vim プラグインEmacs mode があり, IntelliSense 補完やオンザフライでのシンタックス/コンパイルエラーチェック, 定義されているソースへのジャンプなどを使うことができます.

また, Ionide という VSCode 用の F# 拡張機能があり, こちらでは上に加えて CodeLens での型シグネチャ表示やマウスオーバーでの型表示, GUI でのデバッグなどもすることができます.

なお搭載されている補完エンジン自体はすべて共通のもので, Visual Studio のものよりは賢くないですが十分便利です.

.NET Core はパッケージの追加やアプリケーションの実行などで dotnet コマンドを多用するので, そこらへんはむしろ *nix のほうが楽まであります.

Q. 型システムが弱いって聞いた

A. そんなことはない

F# にはインライン関数というものがあって, コンパイル時に消えるのをいいことに, その内部では本来の .NET の型システムでは許されないような様々な暴挙を働くことができます.

どんなことができるのかは わたしの ブログ とかみてください. 誤解を恐れずに言えば, Haskell の型クラス5と同等の機能があります.

ブログの記事でやっているようなことを使って Haskell における様々な概念を使えるようにしたライブラリが, 上で挙げた FSharpPlus です. なお F# Foundation 公式プロジェクトの1つです.

Q. VM 言語だし(例えば OCaml や Haskell より)遅いのでは?

A. そんなことはない6

これはただのベンチマークなので7そこまで参考になるわけではないですが, 一般に言って .NET/F# にはパフォーマンス上他の処理系より利点となりうる要素がそれなりにあります.

1. boxing を極力排除できる/される

.NET では値型と参照型が区別されており, 前者に対する操作は unboxed なまま行われます. また JVM と異なりユーザが値型を定義して使うことができます(struct). F# においても, 再帰的でない代数的データ型やレコード型を任意に値型にすることができます.

また .NET はバイトコードレベルで1階の型システムを積んでいて8, 型変数が値型で具体化される際には専用のコードを JIT で生成して余計な boxing/unboxing が発生しないようにします9. これは boxing を使ってパラメータ多相を実現する多くの処理系とは大きく異なる点です. このおかげでハッシュテーブルなどの多相な(ジェネリックな)データ構造はそのような処理系(OCaml, Haskell を含む)と比べてかなり高速です
10.

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 はこの非同期処理の結果を返す文です.11

Reactive ExtensionsControl.Observable モジュールが標準ライブラリに用意されていますが, FSharp.Control.Reactive パッケージを導入することで observe コンピューテーション式も使えるようになり, さらに簡単に扱えます.

また他にも C# で使われる Task や並列計算を行う Parallel.For, 生の Thread なども扱うこともできます. 実際冒頭で示したベンチマークでも, 多くのケースで F# はこれらの機構を使って OCaml よりも CPU load を最適化しています.

3. インライン関数がある(F#)

上でも触れましたが, F# では再帰的でない関数に let inline というキーワードを用いることで, その呼び出しのインライン展開を強制することができます. これはもちろんパフォーマンス上の利点にもなります.

F# のインライン関数はバイナリにコンパイルされてもメタデータとして残っているので, バイナリで配布されているライブラリからでもインライン展開できるのが特徴的です.

開発・実行環境: .NET Core & VSCode

特長

.NET Core は最新の .NET 実装なだけあって, 今までの実装にはない特長が数多くあります.

1. クロスプラットフォーム

  • Mono と同じく, .NET Core は Windows, OS X, Linux のどのプラットフォームでも全く同じ開発・実行環境を使うことができます.
  • プラットフォーム依存なコードを書かない限り, プラットフォーム依存な問題は発生しません.
  • VSCode などのクロスプラットフォームなエディタを使えば, たとえ短期間で異なる OS 環境を行き来するような事態になっても, ソースコードを持ってくるだけでそのまま作業を続行できます12.

2. 高速な動作, 簡単な配布

  • .NET Core は現状で最も高速な .NET 実装の1つで, ベンチマーク上では例えば Go とほぼ同等〜少し速い程度のパフォーマンスを発揮します.
  • .NET Core は Go と似たようなスタンドアロンバイナリの生成をサポートしており, .NET Core ランタイムがインストールされていない Windows / OS X / Linux 環境上で実行可能な状態で配布することができます.

3. ツール類が dotnet コマンドに集約されていて, ほとんどの操作がこれだけで完結する

  • .NET Core には dotnet という CLI ツール が同梱されており, Rust における cargo コマンドと同様の立ち位置・同等の強力な機能13を備えています.
  • SDK 自体に同梱されているので何もしなくても使えますし, コンパイラを作っているのと同じところが作っているので余計な互換性問題を考えなくて済むのも利点です(cargo と同じように).

インストール

.NET Core SDK は上述の通り, ここ からインストールすることができます. *nix では何らかのパッケージマネージャに乗っかっておいたほうがアップデートが楽ですが, 直接ダウンロードするバイナリは root 権限がなくても使うことができます.

VSCode は ここ からインストールすることができます. 同様にパッケージマネージャに乗っかると楽です.

F# は .NET Core SDK に標準搭載されているので, コンパイルするだけならこれ以上のインストールは必要ありません. VSCode で F# を書くには拡張機能の Ionide が別途必要です14.

なお, エディタは前述の通り補完エンジンが共通なので 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>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

</Project>

3. obj/ … コンパイル時のキャッシュなどが入るディレクトリです.

基本的に触る必要はなく, 配布する必要もないので .gitignore などに入れておくべきです.

さて, コンパイルして実行してみましょう. と言いましたが, 実行するコマンドを打つだけで, 必要な場合15は勝手にコンパイルが走ります.

$ dotnet run
Hello World from F#!

ではパッケージを追加してみましょう. 拙作の FSharp.Scanf ライブラリ16を導入して, フォーマット付きで入力を受け付けられるようにしてみます.

$ 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>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Compile Include="Program.fs" />
  </ItemGroup>

  <!-- ↓New! -->
  <ItemGroup>
    <PackageReference Include="FSharp.Scanf" Version="2.2.6831.16169" />
  </ItemGroup>

</Project>

では, Program.fs を書き換えてみましょう.

open FSharp.Scanf

printfn "what is the ultimate answer?"

try
  let ans = scanf "%i"
  if ans = 42 then
    printfn "correct."
  else
    printfn "%i? no." ans
with
  | _ -> printfn "you entered something other than a number."

わたしは 2 spaces でインデントするのが好きなのでそのように書き換えました17. また今回はコマンドライン引数が要らないのでエントリーポイントは使いません.

では, 実行してみましょう.

$ 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)

のようにすれば, 使う側からは Afh を持つ単一のモジュールに見えます.

また複数ファイルからなる F# プログラムでは, 上の例のように, 各ソースファイルの先頭に名前空間宣言 (namespace Foo) もしくはモジュール宣言 (module Foo) を置かなければなりません. 以後に続くトップレベルの全てが先頭で宣言した名前空間/モジュールの中身に入ることになります. 詳しい解説は MSDN にあります.

F# の特徴的な言語機能

オフサイドルール

F# は Python や Haskell などと同様にオフサイドルールを採用しており, インデントが構文解析に大きく影響する代わりに, 元となった OCaml より記述量が少なく済むようになっています18.

具体的には, 次のように簡略化されます.

// 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 ディレクティヴをファイルの先頭に置いてあげると使えます. この場合はインデントは構文に影響しなくなり, その代わりに様々な場所で inend などが必要になります.

オフサイドルールについては F# syntax: indentation and verbosity - F# for fun and profit に詳しいです.

Computation Expressions

コンピューテーション式(Computation Expressions) は定型的な関数呼び出しに対していい感じな DSL を定義して使うことができる機能です19.

例えば, 九九を列挙する遅延リストも:

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"); ...]

async による非同期処理も:

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 の仕組みを使って実装されています.20

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")
    }
  }

モナディックにコマンドラインアプリを組めるライブラリ21を作れたりします:

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# においては任意の型にメンバ変数/関数を追加することができます22.

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# のほとんど全ての組み込み演算子23はインライン関数として定義されています24 :

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 だけでなく, データ型の型変数もメンバ制約を持つことができます25.

type Hoge< 'a when 'a: (member Piyo: string) > = ...

なお, F# では等値判定 (equality) と大小比較 (comparison) の実装を要求する制約は特別扱いされており10, ^X when ^X: (static member (=): ... ) ではなく ^X when ^X: equality のように書きます26.

よって, 集合を表すデータ型 Set には次のような制約が付いています27.

type Set< 'a when 'a: comparison > = ...

インライン関数と SRTP によるアドホック多相機能を存分に活かすことで, 上で何度も紹介している FSharpPlusわたしのブログ記事 が実現されています.

MSDN に インライン関数, SRTP, 型変数に加えられる制約, 演算子のオーバーロード についてのドキュメントがあります.

Type Providers

型プロバイダ (Type Providers) は, コンパイル時にわかる情報から自動的に型を生成する仕組みです.

メタプログラミングとしてはありがちな(ほんとか?)仕組みですが, 言語自体がサポートしているのが良い点です.

例えば JSON を返す URI を FSharp.Data の JsonProvider に渡すと, バックグラウンドで動作しているコンパイラサービスの働きにより, IDE 上でリアルタイムで補完が効き始めます!

FSharp.Data ウェブサイトより, JSON Provider が動いている様子

  • JSON や XML などをサンプルデータから型を生成して扱える FSharp.Data
  • Azure Storage 上のアセットを補完付きで様々に操作できる Azure Storage Type Provider
  • SQL データベースアクセスを型安全に行える SQLProvider
  • OpenAPI 2.0 に対して API ラッパーライブラリを自動生成できる SwaggerProvider 28 とその後継の 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 Lcm^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 ではそれができません29.

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 variablesreference cells については Wikibooks の F# Programming/Mutable Data もご覧ください(少し古く, let mutable がスコープを抜けるとコンパイルエラーになっていた時代のものですが).

Byrefs, byref-like Structs

Byrefs

Byrefs関数の引数のみに使うことができる ref 型の変種で, 次の3種があります.

  1. 't inref ... 型 't の値を読み取ることだけができるポインタ
  2. 't outref ... 型 't の値を書き込むことだけができるポインタ
  3. 't byref ... 型 't の値を読み書きできるポインタ

これにより, ポインタを受け取る際に細かなアクセス制御をかけることができます.

そして, Byrefs を引数に取る関数に実際に渡すことができるのは次の2つです.

  1. 't ref 型の値
  2. 変数 v があるとき, そのポインタ &v

2 に関しては, let v = 10 のように再代入不可の変数として宣言された場合は inref としてのみポインタを取ることができ, let mutable v = 10 のように再代入可能な変数として宣言された場合は outrefbyref としてもポインタを取ることができます. また, 変数のポインタは変数が定義されたスコープから出ることができません.

byref-like Structs

byref-like structs とは, スタックに確保される値型です. byref-like structs は使用可能な場所が限られていたり30, クロージャでキャプチャできなかったりなどの制限がある代わりに, 高いパフォーマンスを要求される処理に極めて有効です.

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 できなかったりクロージャでキャプチャできなかったり31して不便なので, 少しパフォーマンスを犠牲にしてそれらを可能にするための型です.

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 を参照 するなどしてください.

余談: だいたい全部 F# でできる

fsharp.org のガイドを見ていただければわかるとおり, F# では非常に多くのことをすることができます.

わたしは最近はもう全部 F# でいいんじゃないかなとなっていて, F# だけ書いてます.

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 なので34英語版前提で話を進めます.

MSDN は歩き方を知っていないと, どこ見れば欲しい情報が乗ってるのかわからなく迷子になりがちなので35, 軽く解説します.


  1. ヴァーチャル F# エヴァンジェリストになりたいので雇ってください. 藍沢家という前例があるので 

  2. 2004年にv1.0が出て現在はv5.14が最新です. わたしは 2010 年ごろから使っています. 

  3. v1.0は2016年で現在はv2.0です. え, PCL? 悲しい事件だったね…… 

  4. そもそも C# と F# は同じ .NET の型システムを使っているので, どちらからどちらで書かれたライブラリでも使うことができます. 需要が大きいライブラリはまず .NET Standard に対応しているため, 本当に Windows でしか動かないようなもの以外はほぼ全て利用可能です. 

  5. "GHCの" ではない: Orphan instances が完全に許されない. でももしかしたらそのうちできるようになるかも…… 

  6. CoreRT という .NET Core の AOT コンパイラがありますが, 未だ開発途上なため F# コードの実行にはまだ少なくない制限があります 

  7. かなり有名なサイトではある 

  8. まぁこれが F# の型システムを容易には拡張できない足枷にもなっているのですが 

  9. 詳しくは Andrew Kennedy and Don Syme. The design and implementation of generics for the .NET Common Language Runtime を参照. 

  10. .NET との兼ね合いなど様々な事情により, F# の標準ライブラリの Map, Set などを含む F#の 等値判定と大小比較を使うコードではインライン化 (F#レベル/ILレベル) の恩恵を受けることができず, ユーザ作成の代数的データ型やレコードに用いたときに不必要な boxing/unboxing が発生する ことがある (事情が複雑過ぎてわたしには把握しきれない). これはユーザレベルでならworkaround が存在し, 包括的には F# vNext で改善される見込み. C# の標準ライブラリの Dictionary などを使えば, 現行バージョンでもこの問題は起きない(はず). 

  11. モナドで通じる人向け: bind と return です. 

  12. 実体験です 

  13. テンプレートからの新規作成, ビルド, 実行, テスト実行, パッケージのインストール・作成・アップロード, ユーザによるプラグイン機能, npm でのような CLI ツールのインストール etc. 

  14. VSCode で書くには別途言語サポート拡張機能が必要なのは C# など他の言語でも同じですが 

  15. まだコンパイルしたことがない, ソースコードが変更された, etc. 

  16. F# には printf はあるが, 入力に関しては文字や文字列単位で読み込むもの(System.Console.ReadLine() など)のみがあり, 組み込みでは scanf のような関数がない. この scanf は型安全ではあるが少々ナイーブな実装となっている. 改良求む 

  17. タブインデントは言語仕様レベルで禁止されています.  

  18. 実はわたしはそんなに好きじゃないです. インデントが極端に深くなる書き方を避けようとして, 直し方がわからないエラーに悩まされるので. 

  19. ただの do-notation だと思われるかも知れませんが, 実際は独自構文を定義できたりする関係でより広い範囲の抽象 (MonadPlus, そもそもモナディックでもなんでもない物体, etc) を扱うことができます. 言い方を変えれば, モナドは computation expressions で扱える抽象の1つに過ぎません. 参考: The F# Computation Expression Zoo 

  20. 遅延リスト(seq, IEnumerable<_>)はコンパイラによって特別扱いされてステートマシンに展開するなどの最適化が入ることもある 

  21. 拙作 

  22. クラスベースOOPに馴染みがないならば, 型に密結合したモジュールと考えてください. static memberlet 束縛のようなものでモジュールのように 型名.名前 で呼び出すことができ, member this.hoge値.名前 で呼び出すことができます. 

  23. ! など特定の型にしか使えないものもあるが, ユーザがグローバル名前空間でインライン関数として再定義すれば, 他の型にも使えるようにできる

  24. 実際は特定の組み込み型に対してはコンパイラが直接最適な実装に置き換えるので, そのまま以下のように実装されているわけではない. 

  25. この際の型変数のプレフィックスは ' だが, SRTP と同じ扱いになる. 

  26. 代数的データ型とレコードについては, 関数型などの比較不能な型を含んでいない限りは, 自動で comparisonequality が実装される. 

  27. ちなみに, F# ではモジュールは型ではないため, 型変数を取ってメンバ制約をかけたり, メンバ制約の対象にすることができないのですが, .NET の本来のクラスベースOOの機能を使って, "open できないし内部で型を定義できない代わりに型扱いになるモジュールのようなもの" を作ることができます. これを使えば OCaml のファンクタも一応模倣可能ですが(型変数でモジュールもどきを要求すれば良い), 徹底的にやってる人はわたしは見たことがありません. FSharp.Compatibility.OCaml では型レベルではなく値レベルで模倣しています. 

  28. 知らないうちに死んでた… 

  29. 出典: https://stackoverflow.com/questions/3221200/f-let-mutable-vs-ref 

  30. ライフタイムが有限かつ静的に決まらないといけない. つまり関数の引数やローカル変数には使えるが, クラスのメンバ変数などには使えない. 

  31. これは computation expressions の内部で使うことも含む 

  32. ML(言語)と紛らわしい…… 

  33. さすがに Menhir はないけど…… だれかポートしてくれないかな 

  34. 一部は "Microsoft Docs" であって "MSDN" ではないのですが, MSDN ですね. どこがとはいいませんが. 

  35. docs.microsoft.com と msdn.microsoft.com に分裂しててリンクの貼られ具合によって行ったり来たりする. そのたびにページのレイアウトが変わるため迷子になりやすい