Elixirガードの短絡評価: 条件の順序で結果が変わる
(hauleth.dev)- Elixirのガード(guard) では、
or条件を入れ替えるだけで、同じ論理式に見えるコードの結果が変わることがある is_integer(x) or is_map_key(x,:foo)の順序では、整数入力時に短絡評価が先に起こり、危険な検査をスキップする- 逆に
is_map_key(x,:foo) or is_integer(x)は、整数入力時に最初の条件がfalseではなく失敗するため、後続の条件まで進まない - この違いにより、
Foo.a(%{foo: 21})、Foo.a(37)、Foo.b(%{foo: 21})はtrueだが、Foo.b(37)はfalseになる - ブール演算の交換法則が破れたように見えるが、短絡評価のある
orはもともと条件の順序の影響を受け、Elixir 1.20.1 と OTP 29 の時点では警告もない
条件の順序で結果が変わる例
- 例のモジュール
Fooはa/1とb/1の2つの関数を定義しているa/1:is_integer(x) or is_map_key(x, :foo)の順でガードを評価するb/1:is_map_key(x, :foo) or is_integer(x)の順でガードを評価する- ガードにマッチすれば
true、そうでなければ次の節でfalseを返す
-
a/1: 安全な条件が先に来る場合Foo.a(%{foo: 21})はtrueになるis_integer(x)はfalseis_map_key(x, :foo)はtrueorの結果がtrueなので最初の節にマッチする
Foo.a(37)もtrueになるis_integer(x)がtrueorが短絡評価されるためis_map_key(x, :foo)は実行されない
-
b/1: 失敗しうる条件が先に来る場合Foo.b(%{foo: 21})はtrueになるis_map_key(x, :foo)がtrue- 後ろの
is_integer(x)は実行されない
Foo.b(37)はfalseになる- 最初の条件
is_map_key(x, :foo)はfalseを返すのではなく失敗する - ガード関数1つの失敗は
falseに変換されず、ガード式全体を失敗させる - 後ろの
is_integer(x)は呼ばれず、最初の節にもマッチしない
- 最初の条件
短絡評価と警告の不在
- 多くのElixir開発者にとって、この挙動はブール演算子の交換法則が破れたように見えるかもしれない
- しかし
orは短絡評価を行うため、2つの条件の位置を入れ替えても常に同じ結果になるとは限らない - 基準環境はElixir 1.20.1, OTP 29で、この問題について Elixir は警告を出さないようだ
1件のコメント
Lobste.rs のコメント
Elixir プログラマーではありませんが、最後の例でいちばん驚いたのは、ガード式のエラーが呼び出し元に伝播せず、そのガードが「スキップされる」点です。
なぜそうしたのかは分かる気がしますが、直感に反する結果が出るのも不思議ではありません。
Erlang の API 設計が、Armstrong の Erlang thesis p109/s4.5 で述べられている意図的プログラミングを助けるためのものだったことを考えると、皮肉です。
論文では
dict:fetch(Key, Dict)、dict:search(Key, Dict)、dict:is_key(Key, Dict)のように、プログラマーが「キーは必ず存在するはずだ」「存在するかもしれないので分岐する」「存在の有無だけを調べる」という意図を表す関数を分けて説明しています。ところが Elixir の
is_map_key/2は、「dict」引数が dict でなければ例外を出し、その例外による失敗がガード節全体の失敗につながるため、この区別を壊しているように見えます。逆に、
orが例外を捕捉してfalseにまとめる言語があるとしたら、別の場合にはもっと驚きそうでもあります。is_map_key/2は実のところ、ごく普通の Erlang 関数です。https://www.erlang.org/doc/apps/erts/erlang.html#is_map_key/2
以前見たこの議論のおかげで、今回のクイズを解く準備ができていて、そのときにいくつか学びました。
学びはありましたが、なぜ Pratchett への言及を避けたのか残念です。
Death がどこかで額に手を当てていそうです。
ここで興味深い点は二つあって、
falseではなく失敗したガードは式全体を失敗させること、そしてやや直感に反してis_map_keyがis_mapチェックを内包していないことです。is_map(x) and is_map_key(x, :corporal)のように3つ目のバリエーションを追加すると、期待どおりに動作します。is_map_keyの動作は少し一貫性がないように見え、そのため驚きに感じられます。他のis_...ガードも、安全なものと、型の期待を前提に評価しなければならないものがあるのか確認してみると面白そうです。is_map_keyが特定種類の引数を要求する唯一のis_ガードのようです。他の
is_関数はブール値的な性質を内包していて、常にtrue | falseを返し、失敗しません。ここで興味深い Elixir スタイルの疑問が生まれます。
例は面白く、説明もよくできていますが、個人的には可能ならガードよりパターンマッチを好みます。
もちろん例外はありますが、こうした関数は普通、
def a(%{foo: _x}), do: true、def a(x) when is_integer(x), do: true、def a(_), do: falseのような複数の関数節で書いたと思います。あわせて読むとよさそう: https://learnyouahaskell.github.io/syntax-in-functions.html/…
Haskell ではガードの中で任意の関数を呼び出せますが、Erlang はそこで許可される関数の集合を制限しています。