pa_monad の "unit binder" を少し拡張
OCaml もモナドをつかったプログラミングでは、私は別に bind operator (>>=)
を使うのは全く苦じゃないんです。例えば、今私が遊んでいる LLVM のコードは builder をモナドにしてこんな感じになっています:
let run clos lty_ret = B.cast clos lty_generic_clos_ptr >>= fun clos -> B.const_load clos [0; pos_code_ptr] "code" >>= fun loaded -> B.cast loaded (L.Type.pointer (L.Type.function_list lty_ret [ L.Type.void_pointer; L.Type.void_pointer ])) >>= fun code_ptr -> B.const_load clos [0; pos_env_ptr] "env" >>= fun env_ptr -> get_arg_ptr clos (L.Const.int 0) >>= fun args_ptr -> B.check_call code_ptr [ env_ptr; args_ptr ] >>= fun () -> B.call code_ptr [ env_ptr; args_ptr ] "called"
でも、こういうコンサバな書き方は耐えられない、Haskell の do
記法じゃないとヤダヤダ、という人もいるのですよね。 OCaml では、 pa_monad という CamlP4 拡張を使うと perform
記法というのが使えます (http://www.cas.mcmaster.ca/~carette/pa_monad/):
let run clos lty_ret = perform clos <-- B.cast clos lty_generic_clos_ptr; loaded <-- B.const_load clos [0; pos_code_ptr] "code"; code_ptr <-- B.cast loaded (L.Type.pointer (L.Type.function_list lty_ret [ L.Type.void_pointer; L.Type.void_pointer ])); env_ptr <-- B.const_load clos [0; pos_env_ptr] "env"; args_ptr <-- get_arg_ptr clos (L.Const.int 0); B.check_call code_ptr [ env_ptr; args_ptr ]; (* It is "unit binder" *) B.call code_ptr [ env_ptr; args_ptr ] "called"
なかなかいいね。でも、ちょっと pa_monad で遊んでみたら、いくつか "unit binder"、つまり、 <--
の無い式(何て言うのか知らないから unit binder という名前をつけましたよ)で問題がありました。
OCaml の sequence expression は perform 記法では書けない
perform 記法は perform e; e; e; e; ...
っていう形をしていて、これはオリジナルの OCaml の sequence の文法 e; e; e; ...
を perform
キーワード以下で特殊なパースをする事で実現されているんです。だから、逆に普通の OCaml sequence e; e; e; ...
を perform 記法の中で書けない。その代わり、 let () = e; e; e in
とか書かないといけませんでした:
let run clos lty_ret = perform clos <-- B.cast clos lty_generic_clos_ptr; let () = prerr_endline "clos done" in loaded <-- B.const_load clos [0; pos_code_ptr] "code"; let () = prerr_endline "loaded done" in code_ptr <-- B.cast loaded (L.Type.pointer (L.Type.function_list lty_ret [ L.Type.void_pointer; L.Type.void_pointer ])); env_ptr <-- B.const_load clos [0; pos_env_ptr] "env"; args_ptr <-- get_arg_ptr clos (L.Const.int 0); let () = prerr_endline "ptrs done"; prerr_endline "all things are prepared. Now call!" in B.check_call code_ptr [ env_ptr; args_ptr ]; (* It is "unit binder" *) B.call code_ptr [ env_ptr; args_ptr ] "called"
ああ、ちなみに let _ = e in
と書かないように。これだと e
の結果が何であれ、捨てられてしまい、バグの元です。その代わり、 let () = e in
を使って、結果は unit
型だと確実にしましょう。まあ、どちらにせよ、 let () = ... in
って打ち込むのはうざいよね。だから、 \
で e;
式をエスケープすると普通の sequence にするようにしました:
let run clos lty_ret = perform clos <-- B.cast clos lty_generic_clos_ptr; \ prerr_endline "clos done"; loaded <-- B.const_load clos [0; pos_code_ptr] "code"; \ prerr_endline "loaded done"; code_ptr <-- B.cast loaded (L.Type.pointer (L.Type.function_list lty_ret [ L.Type.void_pointer; L.Type.void_pointer ])); env_ptr <-- B.const_load clos [0; pos_env_ptr] "env"; args_ptr <-- get_arg_ptr clos (L.Const.int 0); \ prerr_endline "ptrs done"; \ prerr_endline "all things are prepared. Now call!"; B.check_call code_ptr [ env_ptr; args_ptr ]; (* It is "unit binder" *) B.call code_ptr [ env_ptr; args_ptr ] "called"
なかなか見栄えが良いでしょう? \
を使えば、OCaml の普通の sequence と unit binder が簡単に見分けがつきますね。
Unit binders は unit monad だけを bind すべき。他の型だったら警告を出したい
もひとつ unit binder で気づいたのは、どんな型のモナドでも警告なしに受け付けてしまう所。これは超危険ですよ。もし式が t monad という、unit と違う t という型を持っているとしたら、その t は何か意味があるはず。それをポイッと捨てるのはバグの元です:
let bind x f = match x with | Some v -> f v | None -> None let return x = Some x let the_answer = return 42 perform the_answer; (* 42 is gone! *) return 666
これは実は pa_monad が unit binder e;
を bind e (fun _ -> ...)
という式に展開する所に問題があります。上の例では bind the_answer (fun _ -> return 666)
ですね。ワイルドカード _
が 42 は何の警告もなく捨てられてしまいます! この展開をちょっとかえて、unit monad じゃない型の bind を unit bind した時は警告を出すようにしました:
let bind x f = match x with | Some v -> f v | None -> None let return x = Some x let the_answer = return 42 perform the_answer; (* Warning S: this expression should have type unit. *) return 666
新しい展開では bind the_answer (fun __should_be_unit -> __should_be_unit; return 666)
という式になり、もし __should_be_unit
が unit
型じゃなければ OCaml コンパイラが警告を出してくれる訳。
pa_monad_custom
改造したのは https://bitbucket.org/camlspotter/pa_monad_custom に置いときました。