学習の記録ー12(コンピュテーション式、Computation Expressions)

F Sharp Programming/Computation Expressions - Wikibooks, open books for an open world
がすごい解り易かったので適当にメモりつつ、コード写経。話の流れとしては

  • 普通の書き方⇒モナド前夜的な書き方⇒モナド(F#のコンピュテーション式)使った書き方⇒F#の糖衣構文使った書き方

という形。

モナドってなんや

F#のComputation Expressions(以下、コンピュテーション式)はHaskellMonadにインスパイアされたものであって、(この側面だけがやたら一人歩き&強調されてる感あるが)数学でいう圏論モナドちゅーもんが根っこのアイディアにある。なんで、この文章だとコンピュテーション式=(Haskellでいう)モナドと考えてよい*1モナドは一言でいうなら関数を実行し、その戻り値を別の関数へ渡すという事を意味する語だそうです。

Any monad

HaskellはすべてのコードをLazyに実行する、つまり必要となるまでそのコードを評価しないという意味で面白い関数型言語になっている。
これはHaskellのユニークな特徴ではあるけど、まぁ逆にめんどくさい問題を引き起こす原因にもなっていて、例えばコードを書いた順番に上から逐次実行していくことが担保されないといった事が生じるわけです。


これをmonadというアイディアを使って解決するためにまずは、よくある「名前を標準入力から入力させて、その結果を標準出力に返す」コードを書く(普通の書き方)。

open System.Net
open System.IO
[<EntryPoint>]
let main args =
    let read_line() = System.Console.ReadLine()
    let print_string(s) = printf "%s" s
    print_string "貴方のお名前、なんてーの? : "
    let name = read_line()
    print_string("こんにちは、" + name + "さん\n")
    0

これを等価な動作をする以下のような形に書きかえる(モナド前夜的な書き方)。要するに定義した関数の引数として関数を取るようにしたというもの。

open System.Net
open System.IO
[<EntryPoint>]
let main args =
    let read_line(f) = f(System.Console.ReadLine())
    let print_string(s, f) = f(printf "%s" s)
    print_string("貴方のお名前、なんてーの? : ", 
        fun _ -> read_line(
            fun name -> print_string("こんにちは、" + name + "さん\n",
                fun _ -> ()
            )
        )
    )
    0

こう書くことによって全てを遅延評価するHaskellにおいても、(世間で良く使われているC++JAVA・なんかでは当たり前にできる)逐次実行を再現できるという形になっている。
この時、print_string・read_line関数は

  • (System.Console.ReadLine()やprintf "%s" sといった)関数を実行し、その戻り値を別の関数(f)へ渡す

となっているので先ほど、上で書いたような荒い意味でのモナドになっているぞとそういうこと(だと理解しているの)です。

Maybe monad

次にMaybeモナドちゅーもんを作ってみる。

普通の書き方

まずは引数の型・範囲(0〜100)チェックして、単純にその和を計算するプログラムを書いてみる。

open System.Net
open System.IO
let addThreeNumbers()= 
    let getNum x = 
        printf "%s" x
        match System.Int32.TryParse(System.Console.ReadLine()) with
        | (true, n) when n >= 0 && n <= 100 -> Some(n)
        | _ -> None
    match getNum "#1: " with
    | Some(x) ->
        match getNum "#2: " with
        | Some(y) ->
            match getNum "#3: " with
            | Some(z) -> Some(x+y+z)
            | None -> None
        | None -> None
    |None -> None

[<EntryPoint>]
let main args =
    match addThreeNumbers() with
    | Some(x) -> printf "Sum : %d" x
    | None -> printf "Error"
    0
モナド前夜的な書き方

これを以下のように書きなおす。

open System.Net
open System.IO
let addThreeNumbers() =
    let bind(input, rest) =
        match System.Int32.TryParse(input()) with
        | (true, n) when n >= 0 && n <= 100 -> rest(n)
        | _ -> None

    let createMsg msg = fun () -> printf "%s" msg; System.Console.ReadLine()
 
    bind(createMsg "#1: ", fun x ->
        bind(createMsg "#2: ", fun y ->
            bind(createMsg "#3: ", fun z -> Some(x + y + z) ) ) )

[<EntryPoint>]
let main args =
    match addThreeNumbers() with
    | Some(x) -> printf "Sum : %d" x
    | None -> printf "Error"
    0

bind関数の引数であるinput関数の結果(標準入力からのデータ)をrest関数のパラメーターとして渡しているのが味噌の作りになっている。

モナド使った書き方

コンピュテーション式は上で示したものと概念的には同じではあるものの、こちゃこちゃ書いてる複雑&うざい部分を隠しておいてくれる。
F#だとモナドはBind・Delay・Returnという3つのメソッドを持つクラスとして定義される。

open System.Net
open System.IO
type MaybeBuilder() =
    member this.Bind(x, f) =
        printf "Bind: %A\n" x
        match x with
            | Some(x) when x >= 0 && x <= 100 -> f(x)
            | _ -> None
    member this.Delay(f) = f()
    member this.Return(x) = Some x

[<EntryPoint>]
let main args =
    let maybe = MaybeBuilder()
    maybe.Delay(fun _ ->
        let x:int = 11
        maybe.Bind(Some(22), fun y ->
            maybe.Bind(Some(33), fun z ->
                maybe.Return(x + y + z)
            )
        )
    ) |> printf "%A"
    0
F#の糖衣構文使った書き方

これはこれで結構書き方がうざったいんで、F#にはこれを簡略化させるためのシュガーシンタックス(糖衣構文)が用意されている。

open System.Net
open System.IO
type MaybeBuilder() =
    member this.Bind(x, f) =
        printf "Bind: %A\n" x
        match x with
            | Some(x) when x >= 0 && x <= 100 -> f(x)
            | _ -> None
    member this.Delay(f) = f()
    member this.Return(x) = Some x

[<EntryPoint>]
let main args =
    let maybe = MaybeBuilder()
    maybe {
        let x = 11
        let! y = Some 22
        let! z = Some 33
        return x + y + z
    } |> printf "%A"
    0

let! y = ...という記法でMaybeBuilderのBind関数を呼ぶイメージ。
同様にreturn ...ちゅー書き方はmaybe.Returnを呼び出しているということですな。
その他の糖衣構文は参照先のページに事細かに書いてある。

【昔、この辺の話が全く理解できなくて足掻いていた時の記録】

*1:LINK先だとワークフローも同義だとしてる