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"

でも、こういうコンサバな書き方は耐えられない、Haskelldo 記法じゃないとヤダヤダ、という人もいるのですよね。 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_unitunit 型じゃなければ OCaml コンパイラが警告を出してくれる訳。

pa_monad_custom

改造したのは https://bitbucket.org/camlspotter/pa_monad_custom に置いときました。