(||) の罠

I間さんの舎弟になるらしい id:wpwhttp://d.hatena.ne.jp/wpw/20081007/1223363005 なんていう落とし穴に引っかかっているのを鼻で笑っていたら、自分自身もやってしまった。

注意 labeled argument 使ってます。適用に読み替えてね。

List.fold_left ~init:false targets ~f:(fun st target ->
  st || do_something_and_return_a_bool target)

targets の要素に副作用を起こす do_something_and_return_a_bool をそれぞれ適用して、ひとつでも true を返したかどうか判定したかったのだが、実際には、一度 st が true になると残りの targets に関する副作用を処理してくれない。(左辺 || 右辺) は左辺が true なら右辺は実行しないのだ。当たり前だが。間違えやすい所だ。
右辺も実行してもらうには、

List.fold_left ~init:false targets ~f:(fun st target ->
  let res = do_something_and_return_a_bool target in
  st || res)

とするべきだったのだ。このバグを見つけるのに2時間かかった。
このミスは数年に一度やってしまう。一度書いてしまうと見つけにくいバグだ。
やですねー。皆気をつけようね、で終わりではあまりにレベルが低いので、

対処法を考える:

boolean で fold しない

List.exists id (List.map ~f:do_something_and_return_a_bool targets)

こういうとき、boolean を使わない。

true, false の代わりに、`Ok, `Error とか使えば ||, && は使わないし。

||, && の意味を変える

必ず右辺も評価することにする。Flow control したければ if-then-else でよい。もちろん、いわゆる ||, && の慣用とは違うわけだけど、もうこの種のバグで苦しみたくないから、俺的には知ったこっちゃねぇよ。

let (&&) = (&&)
let (||) = (||)

で解決できる。一見不思議ですね。

||, && の右辺では副作用は起こさない

さすがに ||, && の意味を変えると顰蹙を買うような気もする。だから、まあ、それは勘弁してやろう。その代わり、右辺には副作用が起こる式は書かない、とういか、書けない事にすればいい。
Perl や C みたいに if not b then do_something () を b || do_something () などと書くことは ML ではほとんどないはずだ*1
しかし、OCaml ではある式が副作用を起こすかどうか解析する手軽な方法はない。型システムを大きく変えなきゃなんない。
それはちょっと面倒すぎる。その代わり、||, && の右辺には変数以外の式を書いた場合は warning を出すようにする、位であればコンパイラの修正も簡単だ。

monadic にして副作用を起こさない

だからって Haskell にしなさいという訳ではないんだが、Caml でも monad はうまく使えば便利だよ。

まとめ

個人的には (||) (&&) は普通の binary operator にしちゃうのが一番簡単だと思う。Flow control は if や match でいい。(||) (&&) をそのままオーバーライトするのがいやって人は、

let (|||) = (||)
let (&&&) = (&&)
let (||) = `Do_not_use_classic_bool_ops
let (&&) = `Do_not_use_classic_bool_ops

とすれば、名前が変わるうえに、右辺も必ず実行してくれるし、(||), (&&) を間違って使っても、型エラーになる。エラーメッセージを見れば原因は一目瞭然。

*1:do_something の型は unit -> unit であるはずなので。