F#で謎カウンターを作るとき、let mutableでなくrefを使う(使っていた)のはなぜ?
これはF# Advent Calendar 201419日目のエントリーです。前日のエントリーはid:u_1rohさんの継続は力なり #FsAdvent - かたちづくりでした。
さて、タイトルに謎カウンターなる謎の言葉が出てきますが、クロージャについて調べたことのある人はなんとなくピンとくるのではないでしょうか。そう、クロージャの説明の時に必ず登場するといわれるアレです。*1
//Javascriptによる例 function makeCounter(){ var i = 0; return function(){ i = i + 1; return i; } } var counter = makeCounter(); alert(counter()); // => 1 alert(counter()); // => 2 alert(counter()); // => 3
([JavaScript] 猿でもわかるクロージャ超入門 5 クロージャを作る - DQNEO起業日記を参考にしました)
これをF#でつくるとき、そのまま書くとこんなかんじになります。
(* コンパイルエラーになる *) module FsAdvent let makeCounter () = let mutable i = 0 (* 変更可能な変数を定義 *) (fun () -> i <- i + 1 i ) let counter = makeCounter () [<EntryPoint>] let main _ = printfn "%d" <| counter () printfn "%d" <| counter () printfn "%d" <| counter () 0 (* FsAdvent.fs(5,6): error FS0407: 変更可能な変数 'i' の使用方法に誤りがあります。 変更可能な変数はクロージャでキャプチャできません。変更可能な変数を削除するか、'ref' および '!' を介してヒープを割り当てた変更可能な参照セルを使用してください。 *)
が、これはコンパイルすることができません。
代わりにこう書く必要があります。
(* 正しい書き方 *) module FsAdvent let makeCounter () = let i = ref 0 (* 参照セルを使う *) (fun () -> i := !i + 1 !i ) let counter = makeCounter () [<EntryPoint>] let main _ = printfn "%d" <| counter () (* => 1 *) printfn "%d" <| counter () (* => 2 *) printfn "%d" <| counter () (* => 3 *) 0
今回はなぜこうなるのかについて考えていきます。
F#: let mutable vs. ref - Stack Overflow
によると、関数内部でlet mutableにより定義された変数は、通常スタック領域に確保されるそうです。スタック領域は関数の実行が終わると解放されてしまうため、その変数にはアクセスできなくなります。
一方refを使ってヒープ領域に参照セルを確保すれば、関数の実行が終わっても値がそのまま残ります。
ただ、普通に考えると上の図のように値の場所がわからなくなってしまうように思います。どのようにして解決しているのでしょうか。
正しい書き方の方をコンパイルしたものをILSpyで開いてみます。
モジュールが静的クラスで表現され、そこにmakeCounterなどの関数がメソッドとして定義されています。あと「makeCounter@5」というヘンな名前の内部クラスが定義されていますが、これがmakeCounter関数が返す無名関数の正体です。このクラスをもう少し詳しく見ていきます
internal class makeCounter@5 : FSharpFunc<Unit, int> { public FSharpRef<int> i; internal makeCounter@5(FSharpRef<int> i) { this.i = i; } public override int Invoke(Unit unitVar0) { this.i.contents = this.i.contents + 1; return this.i.contents; } }
FSharpRef型(F#の参照セルの型です)のフィールドと、コンストラクタ、それから実際の関数の中身であるInvokeメソッドから成っています。
public static FSharpFunc<Unit, int> makeCounter() { FSharpRef<int> i = new FSharpRef<int>(0); return new FsAdvent.makeCounter@5(i); //さっきの内部クラスのインスタンスが作られる }
無名関数が生成されるときは、このクラスのインスタンスが作られて、フィールドに参照セルへの参照がセットされます。
図で描くとこんなかんじですかね?
まとめると
- F#ではlet mutableで定義された変数はスタックに確保される
- 関数実行後も値を失わないためには何らかの方法でヒープに値を保存する必要がある。ref(参照セル)は、手軽に作成できて内部の値を変更することもできるので都合がいい*2
- 無名関数は自動生成されたクラスのインスタンスとして表現される。キャプチャした外部の変数はコンストラクタを経てフィールドにセットされる
という感じになります。
……と、ここまでが現行のF#3.1での話です。実は、開発版のF#4.0ではlet mutableを使った
(* F#4.0ではコンパイル可能 *) module FsAdvent let makeCounter () = let mutable i = 0 (fun () -> i <- i + 1 i) let counter = makeCounter () [<EntryPoint>] let main _ = printfn "%d" <| counter () (* => 1 *) printfn "%d" <| counter () (* => 2 *) printfn "%d" <| counter () (* => 3 *) 0
これがコンパイル可能になります。
一体どうなっているのかとILSpyで中を覗いてみたら、こんなかんじでした。
参照セルを使ったものと完全に一致していますね。*3このように自動的に参照セルを使ったものに変換されるようです。