学ぶ!コンピューテーション式(2) ~そもそもコンピューテーション式ってなに?
はじめに
この記事はコンピューテーション式の解説記事ではありません。以下学ぶ!コンピューテーション式(1) ~let!とBind - komorebikoboshiのブログと同文です。
コンピューテーション式とは
式です。
……というボケで終わるわけにはいかないのですが。ここからは実践F#のワークフローの章を参考にしつつ書いていきます。
式とは
式とは評価されると値(データ)を返すものです。で、式の中にはF#内で使われるデータを生成するデータ式というものがあります。例えば、
1 "abcde" true
こういったリテラルは定数式と呼ばれてそれぞれ数値や文字列などといった値を返しますし、
fun x y -> x + y
いわゆるラムダ式と呼ばれるこれも、関数式という関数値を生み出す式です。
F#では、このようなデータ式から生み出されたデータは、letで変数に束縛したり、関数の引数として渡したりできます。
で、コンピューテーション式もこのようなデータ式なのです。データ式なので生み出された値(コンピューテーション値?)は普通にletで束縛したりできますし、関数のボディにできたりします。
type HogeBuilder () = member this.Bind (x,f) = printfn "Bind" f x member this.Return x = printfn "Return" x member this.Delay f = printfn "Delay" f member this.Run f = printfn "Run" f () let hoge = new HogeBuilder() (* コンピューテーション式をcomp(関数)に束縛 *) let comp x y = hoge{ printfn "hoge" let! a = x + y let! b = x * y return (a,b) } (* val comp : x:int -> y:int -> int * int *) comp 1 2 (* 評価結果 Delay Run hoge Bind Bind Return val it : int * int = (3, 2) *)
type HogeBuilder () = member this.Bind (x,f) = printfn "Bind";f x member this.Return x = printfn "Return";x member this.Delay f = printfn "Delay";f member this.Run f = printfn "Run";f () let hoge = new HogeBuilder() (* foo(変数)に束縛 *) let foo = hoge{ printfn "hoge" let! a = 1 let! b = 2 return (a,b) } (* 出力 Delay Run hoge Bind Bind Return val foo : int * int = (1, 2) *) foo (* 評価結果 val it : int * int = (1, 2) *)
あれ、式の変形が起こった直後にその式が実行されている?
コンピューテーション式の意義
実はちょっと思い違いをしていて、「コンピューテーション式を評価して出た値」を評価したときに式の変形が起こると思っていたのだけど……。
……ええと、いきなり式の変形とか言い出したのですが、これがコンピューテーション式の特徴です。コンピューテーション式は、手続き型プログラミングっぽい書き方を、関数型プログラミングの書き方に変換してくれるのです。
なぜそのようなものが必要かというと、関数の返り値を渡していく関数型プログラミングスタイルだと書きにくいプログラムがあるからです。例えば、
let add x y = printfn "x + y" x + y
「x + y」と表示してx + yの値を返すという、C言語のような手続き型のプログラミング言語だと特に難しくないプログラムですが、関数型で書く場合はprintfnの返り値の()(ユニット)が邪魔になります。
こんな感じで
let add x y = printfn "x + y" |> fun () -> x + y
ユニットを引数にしたラムダ式で受けなければなりません。
まあ、このくらいならいいんですが、例えば
let addmulti x y = printfn "z = %d + %d" x y let z = x + y printfn "z(=%d) * 3" z z * 3
こんな感じのプログラムだと大変です。下のコードが動きません。
let addmulti x y = printfn "z = %d + %d" x y |> fun () -> x + y |> fun z -> printfn "z(=%d) * 3" z |> fun () -> z * 3 (* zは定義されてねー、と怒られる *)
これを期待通り動かすにはこうします。
let addmulti x y = printfn "z = %d + %d" x y |> fun () -> (fun z -> (printfn "z(=%d) * 3" z) |> fun () -> z * 3) (x + y)
非常に読みにくいですね。*1
もうひとつ例を挙げます。九九の表を作りたくなったので1から9までの数字のペア(タプル)を作ってほしい、っていう場合は、関数型の書き方だと、
let pair9x9 = seq{1 .. 9} |> Seq.map (fun x -> seq{1 .. 9} |> Seq.map (fun y -> (x,y) )) |> Seq.concat (* => seq [(1, 1); (1, 2); (1, 3); (1, 4); ...] *)
こんな感じになるでしょか。
実は、F#ではこういう手続きっぽい書き方もできます。
let pair9x9 = seq{ for x in 1 .. 9 do for y in 1 .. 9 do yield (x,y) } (* => seq [(1, 1); (1, 2); (1, 3); (1, 4); ...] *)
こちらの方が(どういう仕組みになっているかはさておき)何をしようとしているかはわかりやすいでしょう。
さて、今まで挙げた手続き型っぽいコード、
let addmulti x y = printfn "z = %d + %d" x y let z = x + y printfn "z(=%d) * 3" z z * 3
これや、
let pair9x9 = seq{ for x in 1 .. 9 do for y in 1 .. 9 do yield (x,y) }
これがF#で動くのは、F#が内部で関数型のコード(上述のコードと同じではありませんが)に変換してくれているからです。そして、コンピューテーション式はこの変換ルールに手を加えることができるのです。
例えば、たびたび例に出すMaybeモナド*2では、let!というキーワードに「関数の返り値からSomeの中身を取り出して後の計算で使うために束縛する」という機能を付け加えています。(ちなみに強調した部分が通常のletとの違いです)さらに、Noneが渡されたときは、そこでプログラムの実行を打ち切る(次の関数を呼び出さない)ということまでしています。
コンピューテーション式を使うことで、複雑な分岐を裏側に隠して、手続き型のかたちですっきりと書くことができるようになります。しかも、コンピューテーション式は他から見ればただの式です。なんかおかしなことをしているのはコンピューテーション式の中だけで、プログラムのほかの部分には影響を及ぼしません、およぼさないんじゃないかなあ。
疑問
エントリーの半ばあたりで触れたのですが、コンピューテーション式はどうもプログラムの解釈が始まる前に式の変換が起きてるっぽい。コンピューテーション式は評価されて(第一級の)値を返すデータ式のはずだけど、プログラムの解釈が始まる前に変換されてたらそもそも評価できないんじゃないかなあ(変換後の式が評価される)。あとそれと、式が変換されて、それが本当に周りに影響を及ぼさないのか。うまくラムダ式に包まれているっぽいけど、うーん。
おわりに
今回はグダグダでしたね、すみません。あと話題がlet!(Bindメソッド)だけっていうのも……。もうちょっと学習が進んでからもう一度書いてみようと思います。