1 Punkte von GN⁺ 3 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • In Guards von Elixir kann sich das Ergebnis von Code, der wie derselbe logische Ausdruck aussieht, allein durch das Vertauschen von or-Bedingungen ändern
  • Bei der Reihenfolge is_integer(x) or is_map_key(x,:foo) greift bei Ganzzahl-Eingaben zuerst die Kurzschlussauswertung und überspringt die riskante Prüfung
  • Umgekehrt führt is_map_key(x,:foo) or is_integer(x) bei Ganzzahl-Eingaben dazu, dass die erste Bedingung nicht false ergibt, sondern fehlschlägt, sodass die zweite Bedingung gar nicht mehr erreicht wird
  • Deshalb sind Foo.a(%{foo: 21}), Foo.a(37) und Foo.b(%{foo: 21}) true, aber Foo.b(37) wird false
  • Dadurch kann es so wirken, als wäre das Kommutativgesetz boolescher Operationen gebrochen, doch or mit Kurzschlussauswertung hängt grundsätzlich von der Reihenfolge der Bedingungen ab; unter Elixir 1.20.1 und OTP 29 gibt es dazu offenbar keine Warnung

Beispiel, bei dem die Reihenfolge der Bedingungen das Ergebnis verändert

  • Das Beispielmodul Foo definiert zwei Funktionen: a/1 und b/1
    • a/1: prüft den Guard in der Reihenfolge is_integer(x) or is_map_key(x, :foo)
    • b/1: prüft den Guard in der Reihenfolge is_map_key(x, :foo) or is_integer(x)
    • Wenn der Guard matcht, wird true zurückgegeben, andernfalls liefert die nächste Klausel false
  • a/1: Wenn die sichere Bedingung zuerst kommt

    • Foo.a(%{foo: 21}) wird true
      • is_integer(x) ist false
      • is_map_key(x, :foo) ist true
      • Das Ergebnis von or ist true, daher matcht die erste Klausel
    • Foo.a(37) wird ebenfalls true
      • is_integer(x) ist true
      • Weil or kurzschlussausgewertet wird, wird is_map_key(x, :foo) nicht ausgeführt
  • b/1: Wenn die potenziell fehlschlagende Bedingung zuerst kommt

    • Foo.b(%{foo: 21}) wird true
      • is_map_key(x, :foo) ist true
      • Das nachfolgende is_integer(x) wird nicht ausgeführt
    • Foo.b(37) wird false
      • Die erste Bedingung is_map_key(x, :foo) liefert nicht false, sondern schlägt fehl
      • Das Fehlschlagen einer einzelnen Guard-Funktion wird nicht in false umgewandelt, sondern lässt den gesamten Guard-Ausdruck fehlschlagen
      • is_integer(x) wird nicht aufgerufen und auch die erste Klausel matcht nicht

Kurzschlussauswertung und fehlende Warnung

  • Für viele Elixir-Entwickler kann dieses Verhalten so wirken, als wäre das Kommutativgesetz des booleschen Operators gebrochen
  • Da or jedoch kurzschlussausgewertet wird, kann man nicht davon ausgehen, dass das Vertauschen der beiden Bedingungen immer dasselbe Ergebnis liefert
  • Die Referenzumgebung ist Elixir 1.20.1, OTP 29; offenbar gibt Elixir für dieses Problem keine Warnung aus

1 Kommentare

 
GN⁺ 3 시간 전
Kommentare auf Lobste.rs
  • Ich bin zwar kein Elixir-Programmierer, aber am überraschendsten am letzten Beispiel ist, dass Fehler in Guard-Ausdrücken nicht an den Aufrufer weitergereicht werden, sondern der Guard „übersprungen“ wird.
    Ich glaube zu verstehen, warum das so gestaltet wurde, aber es ist auch nicht überraschend, dass dabei ein kontraintuitives Ergebnis herauskommt.

  • Es ist ironisch, wenn man bedenkt, dass das API-Design von Erlang dazu gedacht war, intentional programming zu unterstützen, wie Armstrong es in seiner Erlang thesis auf S. 109/Abschnitt 4.5 beschreibt.
    In der Arbeit werden Funktionen wie dict:fetch(Key, Dict), dict:search(Key, Dict) und dict:is_key(Key, Dict) getrennt erklärt, um die Absicht des Programmierers auszudrücken: „Der Schlüssel muss vorhanden sein“, „er könnte vorhanden sein, also verzweige ich den Ablauf“ oder „ich prüfe nur, ob er existiert“.
    Bei Elixirs is_map_key/2 scheint diese Unterscheidung jedoch aufzubrechen: Wenn das „dict“-Argument kein dict ist, wird eine Exception ausgelöst, und dieses Exception-Fehlschlagen führt zum Fehlschlagen der gesamten Guard-Klausel.
    Umgekehrt wäre es in anderen Fällen wohl noch überraschender, wenn es eine Sprache gäbe, in der or Exceptions abfängt und zu false zusammenführt.

  • Dank dieser Diskussion, die ich früher gesehen hatte, war ich auf dieses Quiz vorbereitet und habe damals einiges gelernt.

    • Diese Diskussion hat mich dazu inspiriert, diesen Beitrag zu schreiben.
  • Ich habe zwar etwas gelernt, finde es aber schade, dass die Pratchett-Referenz vermieden wurde.
    Death schlägt sich irgendwo vermutlich die Hand vor die Stirn.
    Interessant sind hier zwei Dinge: Nicht false, sondern ein fehlgeschlagener Guard lässt den gesamten Ausdruck fehlschlagen, und etwas kontraintuitiv schließt is_map_key keine is_map-Prüfung ein.
    Wenn man eine dritte Variante wie is_map(x) and is_map_key(x, :corporal) hinzufügt, verhält es sich wie erwartet.
    Das Verhalten von is_map_key wirkt etwas inkonsistent und deshalb überraschend; es wäre interessant zu prüfen, welche anderen is_...-Guards sicher sind und welche man mit zugrunde liegenden Typerwartungen auswerten muss.

    • Der Pratchett-Referenz stimme ich zu, aber hier ist gerade Hitzewelle, daher funktioniert mein Gehirn nicht wie erwartet.
    • Aus Neugier habe ich ein paar Dinge selbst überprüft, und grob betrachtet scheint is_map_key der einzige is_-Guard zu sein, der eine bestimmte Art von Argument verlangt.
      Die anderen is_-Funktionen haben booleschen Charakter und geben immer true | false zurück, ohne fehlzuschlagen.
  • Daraus ergibt sich eine interessante Frage zum Elixir-Stil.
    Das Beispiel ist unterhaltsam und gut erklärt, aber persönlich bevorzuge ich nach Möglichkeit Pattern Matching gegenüber Guards.
    Natürlich gibt es Ausnahmen, aber solche Funktionen hätte ich normalerweise wohl als mehrere Funktionsklauseln geschrieben, etwa def a(%{foo: _x}), do: true, def a(x) when is_integer(x), do: true, def a(_), do: false.

  • Ebenfalls lesenswert: https://learnyouahaskell.github.io/syntax-in-functions.html/…

    • Guards in Haskell sind etwas anders.
      In Haskell kann man innerhalb eines Guards beliebige Funktionen aufrufen, während Erlang die Menge der dort erlaubten Funktionen einschränkt.