komorebikoboshiのブログ

プログラミング記事(趣味レベル)が多め。

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により定義された変数は、通常スタック領域に確保されるそうです。スタック領域は関数の実行が終わると解放されてしまうため、その変数にはアクセスできなくなります。
f:id:komorebikoboshi:20141219100204p:plain
一方refを使ってヒープ領域に参照セルを確保すれば、関数の実行が終わっても値がそのまま残ります。
f:id:komorebikoboshi:20141219100258p:plain
ただ、普通に考えると上の図のように値の場所がわからなくなってしまうように思います。どのようにして解決しているのでしょうか。
正しい書き方の方をコンパイルしたものをILSpyで開いてみます。
f:id:komorebikoboshi:20141219100312p:plain
モジュールが静的クラスで表現され、そこに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:id:komorebikoboshi:20141219100333p:plain
まとめると

  • 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で中を覗いてみたら、こんなかんじでした。
f:id:komorebikoboshi:20141219100349p:plain
参照セルを使ったものと完全に一致していますね。*3このように自動的に参照セルを使ったものに変換されるようです。

*1:アキュムレータというのが正しい?

*2:実際、配列を使っても同じことができる

*3:複雑なプログラムになるとどうなるか分かりませんが