(||) の罠
I間さんの舎弟になるらしい id:wpw が http://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 を出すようにする、位であればコンパイラの修正も簡単だ。
まとめ
個人的には (||) (&&) は普通の 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 であるはずなので。