Twitterで流れてきたListReaderコンピュテーション式の解説
先日、Twitterで id:n7shi さんが面白いコードを投下していた。
コンピュテーション式のビルダーは1回しか書いたことがないので、簡単なものでも苦戦した。用途を限定してリストを順に読む例(モナドではない)。 https://t.co/Zfq0M9vm0n
— 七誌 (@7shi) June 29, 2016
これに対しての私のreplyが以下。
@7shi @callmekohei 面白そうだったのでいじってみました https://t.co/yg7dLSHKs4
— ナゲット・もみあげ (@pocketberserker) June 29, 2016
これらのコンピュテーション式はちょっとわかりづらいのでちょっとした解説を試みる。
7shiさん版ListReaderBuilder
7shiさんのコードに少し手を加えたものが以下のコードである。 元コードとの挙動に差はない。
type ListReaderBuilder() = member __.Bind(_, f) = function | x::xs -> f x xs | [] -> () member __.Zero() = fun _ -> () let listReader = ListReaderBuilder() let test = listReader { let! a = () in printfn "%d" a let! a = () in printfn "%d" a } test [1;2;3] printfn "---" test [4]
このコードの出力は以下のようになる。
1 2 --- 4
このビルダーで使われる変換規則は以下の通り。
T(let! p = e in ce, V, C, q) = T(ce, V ⊕ var(p), λv.C(b.Bind(src(e),fun p -> v), q)
T(do e in ce, V, C, q) = T(ce, V, v.C(e; v), q)
T(e;, V, C, q) = C(e;b.Zero())
test
を展開すると以下のようになるだろう(実際はもっと異なるかもしれないが、私はコンパイラではないのでわからない)。
// val test: int list -> unit let test = // let! a = () in ... の展開 listReader.Bind( (), fun a -> // do printfn "%d" a in ... の展開 printfn "%d" a // let! a = () in ... の展開 listReader.Bind( (), fun a -> // printfn "%d" a; の展開 printfn "%d" a listReader.Zero() ) )
- Bindメソッドのシグネチャは
'T * ('U -> 'U list -> unit) -> ('U list -> unit)
- Zeroメソッドのシグネチャは
unit -> ('a -> unit)
((Bindの型パラメータと異なることを強調したかったのであえて'a
表記にしている)) printfn
部分でa
はintに固定される
ことを考えれば、型に問題はないことに気づくだろう。
考察
面白いコードだが、個人的に気になったことがあった。
let! a = () in ...
がなんだか冗長に見えるのだ。
do! ...
のほうが短くて見やすい気がする。
いじってみた版
type ListReaderBuilder() = member __.Bind(g, f) = function | x::xs -> f (g x) xs | [] -> () member __.Return(_) = fun _ -> () let listReader = ListReaderBuilder() let test = listReader { do! printfn "%d" do! printfn "%d" }
このビルダーで使われる変換規則は以下の通り。
T(let! p = e in ce, V, C, q) = T(ce, V ⊕ var(p), λv.C(b.Bind(src(e),fun p -> v), q)
T(do! e in ce, V, C, q) = T(let! () = e in ce, V, C, q)
T(do! e;, V, C, q) = T(let! () = src(e) in b.Return(), V, C, q)
同じようにtest
を展開してみる。
// val test: int list -> unit let test = // do! printfn "%d" in ... の展開 listReader.Bind( (printfn "%d"), fun () -> // do! printfn "%d"; の展開 listReader.Bind( (printfn "%d"), fun () -> listReader.Return(()) ) )
- Bindメソッドのシグネチャは
('T -> 'U) * ('U -> 'T list -> unit) -> ('T list -> unit)
- Returnメソッドのシグネチャは
'a -> ('b -> unit)
- 最終的に残ったリストはReturnが返すλ式で捨てられる
g x
でprintfn "%d" x
が実行され、その戻り値unit
がf
に渡る
こちらも型は問題ないことがわかるだろう。
やりたいことができるか試す
その後の7shiさんとのやり取りで、以下のようなことがやりたかったというコメントを頂いた。
改変後のコードでも同様の挙動にできるか試してみよう。
ビルダーとaddPerson
のシグネチャの順序、コンピュテーション式を書き換える。
// 変更したコードのみ載せています type ListReaderBuilder() = member __.Bind(g, f) = function | x::xs -> f (g x) xs | [] -> () member __.Return(_) = fun _ -> () ... let addPerson data name = if name <> "" then persons.[name] <- data ... row |> listReader { let! company = id do! addPerson (company, "会長") do! addPerson (company, "社長") do! addPerson (company, "副社長") do! addPerson (company, "専務") } ...
最初の値は他の場所で使いたいのでid
で取得する。
残りはprintfn
の時と同じ要領だ。
実際に試してみると同じ結果を返した。
おわりに
結果は同じでも、ビルダーの定義次第でコンピュテーション式の書き方が変わる。 他言語のマクロのように自由ではない限られた変換で何が表現できるのか、疑問は尽きない。