めざそう言語処理系の沼 〜shift/resetへの旅 〜 その21 やはり俺のListモナドはまちがっている。
前回こう書いた:
ちなみにこの記事を書いている最中にListモナド実装にも重大なミスがあることに気づいた
今回はその話。
reify
の型
Representing Monadsのreify
とreflect
の型定義:
signature RMONAD = sig structure M : MONAD val reflect : '1a M.t -> '1a val reify : (unit -> 'a) -> 'a M.t end;
これを見てみるとreify
は'a
型を返すthunkを引数にとり、'a
を包んだモナディックな値を返す、という型になっている。(前回の記事でも言及したが、Kontlangではreify
はマクロなので(unit -> 'a)
なthunk関数ではなく、評価されたら'a
型になる式をそのまま受け取れる)
それに対して、前々回でListモナドを使ってみた時、このような形で書いた:
(reify (let [(x (reflect (list 1 2 3 4 5))) (y (reflect (list 1 2 3 4 5))) (z (reflect (list 1 2 3 4 5)))] (if (= (* z z) (+ (* x x) (* y y))) (return (list x y z)) nil)))
reify
の中の式が返す値は(return ...)
かnil
で、どちらもモナディックな値(この場合リスト)となっている。((return (list x y z))
は(list x y z)
をreturn
でさらにネストした要素1のリストに変換していることに注意)
これだとreify
の型は(unit -> 'a M.t) -> 'a M.t
となってしまう。
実際reify
の定義もこうなっていた:
(define reify (macro [expr] (reset expr)))
reset
に包むだけで他の変換は何もしていないわけだから、(unit -> 'a M.t) -> 'a M.t
なのも当然とも言える。
そう考えると正しい定義は:
(define reify (macro [expr] (reset (list expr))))
このように式の結果をモナディックな値(Listモナドなら当然リスト)に変換する処理を入れるのが大事。こうすれば(return (list x y z))
からreturn
が省ける。
しかし今度はif
分岐のもう一つの結果であるnil
が問題となってくる。こちらもreify
の(reset (list expr))
でリスト化されてしまって、最終的な結果に残ってしまう(例えば(nil nil ... (3 4 5) nil ... (4 3 5) ...)
のような結果が返ってくる)。
nil
の代わりに「分岐のこちら側は失敗でなんの結果も返さない」という意味の処理が必要となる。
fail
あるいは(reflect nil)
この問題の解決方法もやはりRepresenting Monadsに書かれていた。
fail
という関数を定義して使えばいい:
(letfn [fail [] (reflect nil)] ...)
少し非直観的なのでreflect
関数の挙動を考えてみる。
複数の要素が含まれるリストをreflect
すると、それ以降の処理が各要素につき一回ずつ、バックトラックで行われる。
要素数1のリストをreflect
すると、それ以降の処理がその一つの要素に関してのみ実行される。なので(let [a (reflect (list b))] ...)
は(let [a b] ...)
と変わらない。
では空リストをreflect
するとどうなるのか?それ以降の処理を適用する要素がないので、そのまま実行を打ち切り、なんの結果も返さずにバックトラックする。
この挙動が(if condition result nil)
のnil
の部分に本当に欲しかったものだ。
なので(letfn [fail [] (reflect nil)] ...)
で「空リストをreflect
」を実行する引数0の関数fail
を定義する。
これで「この式が評価されるところまで来たらなんの結果も返さずに処理を打ち切ってバックトラック」というポイントを(fail)
で指定できる。
使ってみる:
修正したreify
と追加したfail
を前述の例で使ってみる:
(reify (let [(x (reflect (list 1 2 3 4 5))) (y (reflect (list 1 2 3 4 5))) (z (reflect (list 1 2 3 4 5)))] (if (= (* z z) (+ (* x x) (* y y))) (list x y z) (fail))))
これでreify
のなかの式は返り値がモナディックではない値でよくなった(わかりにくいことにこのケースではそれでも返り値はリストだが、「モナドとしてのリスト」ではない)。
気になるのはfail
の型だが・・・ なんの値も返さない(nilすら)ので、逆になんの型とも矛盾しないということで() -> 'a
になるのか・・・?まあkontlangは動的型付けなのであまり関係ないが・・・
次回
次は話を戻してモジュールの実装を進めていく。