1 ポイント 投稿者 GN⁺ 3 시간 전 | 1件のコメント | WhatsAppで共有
  • 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 の時点では警告もない

条件の順序で結果が変わる例

  • 例のモジュール Fooa/1b/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)false
      • is_map_key(x, :foo)true
      • or の結果が true なので最初の節にマッチする
    • Foo.a(37)true になる
      • is_integer(x)true
      • or短絡評価されるため 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件のコメント

 
GN⁺ 3 시간 전
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 にまとめる言語があるとしたら、別の場合にはもっと驚きそうでもあります。

  • 以前見たこの議論のおかげで、今回のクイズを解く準備ができていて、そのときにいくつか学びました。

    • その議論に触発されて、この記事を書くことになりました。
  • 学びはありましたが、なぜ Pratchett への言及を避けたのか残念です。
    Death がどこかで額に手を当てていそうです。
    ここで興味深い点は二つあって、false ではなく失敗したガードは式全体を失敗させること、そしてやや直感に反して is_map_keyis_map チェックを内包していないことです。
    is_map(x) and is_map_key(x, :corporal) のように3つ目のバリエーションを追加すると、期待どおりに動作します。
    is_map_key の動作は少し一貫性がないように見え、そのため驚きに感じられます。他の is_... ガードも、安全なものと、型の期待を前提に評価しなければならないものがあるのか確認してみると面白そうです。

    • Pratchett への言及には同意しますが、今は猛暑なので、脳が期待どおりに動いていません。
    • 気になっていくつか自分で確認してみたところ、ざっと見た限りでは、is_map_key が特定種類の引数を要求する唯一の is_ ガードのようです。
      他の is_ 関数はブール値的な性質を内包していて、常に true | false を返し、失敗しません。
  • ここで興味深い Elixir スタイルの疑問が生まれます。
    例は面白く、説明もよくできていますが、個人的には可能ならガードよりパターンマッチを好みます。
    もちろん例外はありますが、こうした関数は普通、def a(%{foo: _x}), do: truedef a(x) when is_integer(x), do: truedef a(_), do: false のような複数の関数節で書いたと思います。

  • あわせて読むとよさそう: https://learnyouahaskell.github.io/syntax-in-functions.html/…

    • Haskell のガードは少し違います。
      Haskell ではガードの中で任意の関数を呼び出せますが、Erlang はそこで許可される関数の集合を制限しています。