komorebikoboshiのブログ

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

学ぶ!コンピューテーション式(1) ~let!とBind

はじめに

最初に御注意を。
このエントリーはコンピューテーション式の解説記事ではありません。komorebikoboshiがコンピューテーション式について勉強して、考えたことを書き留めていく記事になる予定です。よってところどころに間違った内容が入る可能性がありますので、まあ話半分に読んでください。間違いが分かれば随時訂正していく予定です。あと、タイトルの元ネタはもちろん#119 できる!コンピュテーション式 « F# « a wandering wolfです。いつかこの記事を完全に理解できる日が来るといいなあ。

letとラムダ式

で、第1回目はlet!をやります。まあ私がlet!とreturnしか知らないからですが。
突然ですが、このような関数があるとします。コンピューテーション式ではない普通のコードです。

let func1 x =
    let a = x + 1
    let b = a + 2
    b

(* func1 5 => 8 *)

まあ(無駄なことをしていますが)とくに変哲のないコードだと思います。ところで、関数型プログラミングの基本は、ある関数の返り値を別の関数の引数にする関数の連鎖だと思うのですが、このコードでは各行の3つの式が別々にあるように見えます。
このコードは次のコードで置き換えることができます。

let func1 x = (fun a -> (fun b -> b) (a + 1)) (x + 1)

(* func1 5 => 8 *)

ラムダ式の引数をletの代わりに使っています。まあこっちの方が読みにくいのですが、つまりは

let func1 x =
    let a = x + 1
    let b = a + 2
    b

こういう手続きっぽいコードを、

let func1 x = (fun a -> (fun b -> b) (a + 1)) (x + 1)

このように関数の連鎖で書き換えることができるということです。

コンピューテーション式のlet!

では、コンピューテーション式を見ていきます。さっきの関数と同じものをコンピューテーション式で書きます。

type HogeBuilder () =
    member this.Bind (x,f) = f x
    member this.Return x = x

let hoge = new HogeBuilder()

(* ここからが本体 *)
let func1 x = hoge{
    let! a = x + 1
    let! b = a + 2
    return b
}
(* func1 5  => 8 *)

とりあえずletがlet!に変わってreturnがつきました。

let!の展開

さて、このコンピューテーション式なんですが、内部では次のように展開されます。

let func1 x =
    hoge.Bind(x + 1,
                fun a -> hoge.Bind(a + 2,
                                    fun b -> hoge.Return b))

なんとなくx + 1の返り値をfun a -> hoge.Bind ... に渡して~というように見えます。これをBind、Returnメソッドの定義を使ってさらに展開すると、

let func1 x = (fun a -> ((fun b -> b) (a + 2))) (x + 1)

となって結局上述の関数と同じになります。

で、どうなの?

もういちどlet!を展開した式を載せます。ちょっとインデントはいじっていますが

let func1 x =
    hoge.Bind(x + 1,
    fun a -> hoge.Bind(a + 2,
    fun b -> hoge.Return b))

(ちなみにこのようにインデントをするとF#のコンパイラさんに怒られます)
それをさらに展開した式にパイプライン演算子を当てはめた式も載せます。

let func1 x =
    x + 1
    |> fun a -> a + 2
    |> fun b -> b

そっくりですね。ちがうのは下の式が関数の返り値をそのまま次の関数に流しているのに対して、上の式は関数と関数の間にBindメソッドを挟んでいるところです。つまり、このBindメソッドをいじくることで関数の返り値を流す時にちょっと小細工することができるのです!ためしに、Bindメソッドを少し変えてみます。

type HogeBuilder () =
    member this.Bind (x,f) = f (x * 2) (* 2倍に! *)
    member this.Return x = x

let hoge = new HogeBuilder()

(* ここからが本体 *)
let func1 x = hoge{
    let! a = x + 1 (* a = 2 * (x + 1) *)
    let! b = a + 2 (* b = 2 * (a + 2) *)
    return b
}
(* func1 5  => 28 *)

この関数に5を入力すると28を返します。x + 1に5を入れたものを計算した返り値を次の式に渡すのですが、その時返り値を2倍にして渡しています。次も同様で、a(= 12)に2を足して、さらに2倍にしたものを次に流しています。で、最終的には28が返ります。
この前のMaybeモナドの例(「モナドのすべて」のMaybeモナドの例をF#で - komorebikoboshiのブログ)でも、Bindメソッドが返り値のSomeの中身を取り出して次の関数に渡し、その返り値をしれっとSomeで包むという芸当をやっています。

結局

let!がなにをするかというと、関数の連鎖の隙間に入り込んでちょっと小細工する、ような感じだと思います。たぶんモナドをもっと勉強すれば有効な使い方が分かるのでしょうけど、モナド以外でも何かに使えそうな気はします。とりあえず初回としては割と有意義な結論が出たかな?