F#でズンドコした改造版
let zundokoSeq = seq{ let rand = new System.Random () while true do let x = rand.Next(2) (* 0がズンで1がドコ *) if x = 0 then printfn "ズン" else printfn "ドコ" yield x } zundokoSeq |> Seq.windowed 5 |> Seq.takeWhile (fun x -> x = [|0;0;0;0;1|] |> not) |> Seq.toArray |> ignore printfn "キ・ヨ・シ!"
F#でズンドコした
今更感あるけどF#でズンドコした。
type Zundoko = Zun | Doko (* F# 4.0より前のバージョンの場合 let rand = new System.Random () *) let rand = System.Random () let randomZunDoko () = if rand.Next(2) = 0 then printfn "ズン"; Zun else printfn "ドコ"; Doko (* 一つの関数が一つの状態を表す。関数を呼び出すとその関数の状態に遷移する *) let rec state1:(Zundoko -> unit) = function | Zun -> state2 <| randomZunDoko () (* "ズン"なら状態2へ *) | Doko -> state1 <| randomZunDoko () (* "ドコ"なら再び状態1へ *) and state2 = function | Zun -> state3 <| randomZunDoko () | Doko -> state1 <| randomZunDoko () and state3 = function | Zun -> state4 <| randomZunDoko () | Doko -> state1 <| randomZunDoko () and state4 = function | Zun -> state5 <| randomZunDoko () | Doko -> state1 <| randomZunDoko () and state5 = function | Zun -> state5 <| randomZunDoko () | Doko -> printfn "キ・ヨ・シ!" randomZunDoko () |> state1
状態遷移はズンドコキヨシ with DFA(決定性有限オートマトン) - Qiitaを参考にしました。
F#っぽくするなら、ズンドコはシーケンス式で生成してSeq.windowedを使うのだろうけど出力がややこしそうなのでこんな感じにした。
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このように自動的に参照セルを使ったものに変換されるようです。
VS2013でFsYacc、FsLexを使う
Visual Studio 2013*1でFsYaccとFsLexを使うには、基本的に
http://stackoverflow.com/questions/19781156/using-fslex-yacc-in-vs2013
の通りにすればいいのですが、2つほどハマったところがあったのでそれを書いておきます。
FsYacc、FsLexが参照するFSharp.Coreのバージョン
VS2013 + F# 3.1.1しかパソコンにインストールしていない場合、FsYaccやFsLexが起動しません。これはそれらのツールがFSarp.Core.dllのバージョン3.0を読みに行こうとしているのが原因です。
そこで、F#のコンパイラ(Fsc.exe)のあるディレクトリ*2にある「○○.exe.config」というファイルを2つぐらい、プロジェクトのfslex.exeやfsyacc.exeがあるディレクトリにコピーして、それぞれ「fslex.exe.config」「fsyacc.exe.config」とリネームします。これでFsYaccやFsLexはFSarp.Core.dllのバージョン3.1.1を読み込んでくれるようになります。
ファイルの更新順序
今回FsYaccの使い方についてfslexとfsyaccを使って字句・構文解析して電卓っぽい計算をしてくれる奴を作成したい - My Life as a Mock Quantを参考にさせてもらったのですが、この記事の例を試す時に
- Lexer.fslを追加して編集
- Parser.fsyを追加して編集
- Parser.fs、Lexer.fsを追加
- .fsprojファイルをいじる
- ビルド
の順番に作業するとうまくいきません。(FsLex、FsYaccが走らない)理由は、Lexer.fsやParser.fsをLexer.fslやParser.fsyより後に作成している(つまり、より新しい)ため、MsBuildが更新の必要がないとしてFsLexなどのタスクをスキップするためです。(結果、Parser.fsなどの中身が作成されない)
これを回避するには、まあ、Parser.fs、Lexer.fsをLexer.fslやParser.fsyよりも先に追加しておくとか、.fslや.fsyに適当なコメントとかを書いて更新時間を遅くする、とか。おそらく初回のビルド以降では問題にはならないでしょう。
なお、これはMsBuildのコンパイルタスク*3を使うときに問題になりますが、Pre-build eventで直接fslex.exeなどをたたく方法の時は関係ありません*4
まとめ
- FsYaccなどが参照するdllを~.exe.configでごまかす
- ファイルの更新時間に気を付ける。.fsyや.fslの方を新しくする