Kurzschlussauswertung in Elixir-Guards: Die Reihenfolge der Bedingungen verändert das Ergebnis
(hauleth.dev)- 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 nichtfalseergibt, sondern fehlschlägt, sodass die zweite Bedingung gar nicht mehr erreicht wird - Deshalb sind
Foo.a(%{foo: 21}),Foo.a(37)undFoo.b(%{foo: 21})true, aberFoo.b(37)wirdfalse - Dadurch kann es so wirken, als wäre das Kommutativgesetz boolescher Operationen gebrochen, doch
ormit 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
Foodefiniert zwei Funktionen:a/1undb/1a/1: prüft den Guard in der Reihenfolgeis_integer(x) or is_map_key(x, :foo)b/1: prüft den Guard in der Reihenfolgeis_map_key(x, :foo) or is_integer(x)- Wenn der Guard matcht, wird
truezurückgegeben, andernfalls liefert die nächste Klauselfalse
-
a/1: Wenn die sichere Bedingung zuerst kommtFoo.a(%{foo: 21})wirdtrueis_integer(x)istfalseis_map_key(x, :foo)isttrue- Das Ergebnis von
oristtrue, daher matcht die erste Klausel
Foo.a(37)wird ebenfallstrueis_integer(x)isttrue- Weil
orkurzschlussausgewertet wird, wirdis_map_key(x, :foo)nicht ausgeführt
-
b/1: Wenn die potenziell fehlschlagende Bedingung zuerst kommtFoo.b(%{foo: 21})wirdtrueis_map_key(x, :foo)isttrue- Das nachfolgende
is_integer(x)wird nicht ausgeführt
Foo.b(37)wirdfalse- Die erste Bedingung
is_map_key(x, :foo)liefert nichtfalse, sondern schlägt fehl - Das Fehlschlagen einer einzelnen Guard-Funktion wird nicht in
falseumgewandelt, sondern lässt den gesamten Guard-Ausdruck fehlschlagen is_integer(x)wird nicht aufgerufen und auch die erste Klausel matcht nicht
- Die erste Bedingung
Kurzschlussauswertung und fehlende Warnung
- Für viele Elixir-Entwickler kann dieses Verhalten so wirken, als wäre das Kommutativgesetz des booleschen Operators gebrochen
- Da
orjedoch 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
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)unddict: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/2scheint 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
orExceptions abfängt und zufalsezusammenführt.is_map_key/2ist tatsächlich eine völlig normale Erlang-Funktion.https://www.erlang.org/doc/apps/erts/erlang.html#is_map_key/2
Dank dieser Diskussion, die ich früher gesehen hatte, war ich auf dieses Quiz vorbereitet und habe damals einiges gelernt.
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ßtis_map_keykeineis_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_keywirkt etwas inkonsistent und deshalb überraschend; es wäre interessant zu prüfen, welche anderenis_...-Guards sicher sind und welche man mit zugrunde liegenden Typerwartungen auswerten muss.is_map_keyder einzigeis_-Guard zu sein, der eine bestimmte Art von Argument verlangt.Die anderen
is_-Funktionen haben booleschen Charakter und geben immertrue | falsezurü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/…
In Haskell kann man innerhalb eines Guards beliebige Funktionen aufrufen, während Erlang die Menge der dort erlaubten Funktionen einschränkt.