Évaluation en court-circuit des gardes Elixir : l’ordre des conditions change le résultat
(hauleth.dev)- Dans les gardes (guards) d’Elixir, le simple fait d’inverser des conditions avec
orpeut changer le résultat d’un code qui semble pourtant exprimer la même logique - Avec
is_integer(x) or is_map_key(x,:foo), l’ordre permet qu’une évaluation en court-circuit se produise d’abord et évite ainsi une vérification dangereuse pour une entrée entière - À l’inverse,
is_map_key(x,:foo) or is_integer(x)fait que, pour une entrée entière, la première condition n’échoue pas en renvoyantfalse, mais en échouant tout court, sans jamais atteindre la condition suivante - À cause de cette différence,
Foo.a(%{foo: 21}),Foo.a(37)etFoo.b(%{foo: 21})valenttrue, maisFoo.b(37)vautfalse - On pourrait avoir l’impression que la commutativité des opérations booléennes est rompue, mais
oravec court-circuit dépend naturellement de l’ordre des conditions, et aucun avertissement n’est émis avec Elixir 1.20.1 et OTP 29
Exemple où l’ordre des conditions change le résultat
- Le module d’exemple
Foodéfinit deux fonctions,a/1etb/1a/1: vérifie la garde dans l’ordreis_integer(x) or is_map_key(x, :foo)b/1: vérifie la garde dans l’ordreis_map_key(x, :foo) or is_integer(x)- Si la garde correspond, la fonction renvoie
true, sinon la clause suivante renvoiefalse
-
a/1: quand la condition sûre vient en premierFoo.a(%{foo: 21})vauttrueis_integer(x)vautfalseis_map_key(x, :foo)vauttrue- Le résultat de
orvauttrue, donc la première clause correspond
Foo.a(37)vaut aussitrueis_integer(x)vauttrue- Comme
orfait une évaluation en court-circuit,is_map_key(x, :foo)n’est pas exécuté
-
b/1: quand la condition susceptible d’échouer vient en premierFoo.b(%{foo: 21})vauttrueis_map_key(x, :foo)vauttrue- Le
is_integer(x)suivant n’est pas exécuté
Foo.b(37)vautfalse- La première condition,
is_map_key(x, :foo), n’échoue pas en renvoyantfalse, elle échoue simplement - L’échec d’une seule fonction de garde n’est pas converti en
falseet fait échouer toute l’expression de garde is_integer(x)n’est donc pas appelée, et la première clause ne correspond pas
- La première condition,
Évaluation en court-circuit et absence d’avertissement
- Pour beaucoup de développeurs Elixir, ce comportement peut donner l’impression que la commutativité des opérateurs booléens est rompue
- Mais comme
orfait une évaluation en court-circuit, on ne peut pas considérer qu’inverser les deux conditions donnera toujours le même résultat - L’environnement de référence est Elixir 1.20.1, OTP 29, et Elixir ne semble pas émettre d’avertissement pour ce cas
1 commentaires
Commentaires sur Lobste.rs
Je ne suis pas programmeur Elixir, mais ce qui me surprend le plus dans le dernier exemple, c’est que l’erreur dans l’expression de garde n’est pas propagée à l’appelant et que cette garde est simplement « ignorée ».
Je pense comprendre pourquoi cela a été conçu ainsi, mais il n’est pas étonnant que cela produise des résultats contre-intuitifs.
C’est ironique quand on pense que la conception des API d’Erlang visait à aider la programmation intentionnelle dont parle Armstrong dans sa thèse sur Erlang, p. 109/section 4.5.
La thèse distingue des fonctions comme
dict:fetch(Key, Dict),dict:search(Key, Dict)etdict:is_key(Key, Dict), qui expriment l’intention du programmeur : « la clé doit forcément exister », « elle peut exister, donc on sépare le flux », ou « on vérifie seulement son existence ».Or
is_map_key/2en Elixir lève une exception si l’argument “dict” n’est pas un dict, et cet échec par exception entraîne l’échec de toute la clause de garde, ce qui semble casser cette distinction.À l’inverse, s’il existait un langage où
orattrape les exceptions et les fusionne enfalse, ce serait sans doute encore plus surprenant dans d’autres cas.is_map_key/2est en fait une fonction Erlang tout à fait ordinaire.https://www.erlang.org/doc/apps/erts/erlang.html#is_map_key/2
Grâce à cette discussion que j’avais lue il y a quelque temps, j’étais prêt à résoudre ce quiz, et j’y avais appris plusieurs choses.
J’ai appris des choses, mais je regrette qu’on ait évité la référence à Pratchett.
La Mort doit être quelque part en train de se prendre la tête.
Ce qui est intéressant ici tient à deux points : ce n’est pas
false, mais une garde en échec qui fait échouer toute l’expression, et, de façon assez contre-intuitive,is_map_keyn’implique pas un testis_map.Si l’on ajoute une troisième variante comme
is_map(x) and is_map_key(x, :corporal), cela fonctionne comme prévu.Le comportement de
is_map_keyparaît un peu incohérent et donc surprenant ; il serait intéressant de vérifier, parmi les autres gardesis_..., lesquelles sont sûres et lesquelles doivent être évaluées avec une attente de type.is_map_keysemble être la seule gardeis_à exiger un type d’argument particulier.Les autres fonctions
is_ont un caractère booléen implicite et renvoient toujourstrue | falsesans échouer.Cela soulève une question intéressante de style Elixir.
L’exemple est amusant et bien expliqué, mais personnellement, quand c’est possible, je préfère le pattern matching aux gardes.
Il y a bien sûr des exceptions, mais j’aurais généralement écrit ce genre de fonctions avec plusieurs clauses, comme
def a(%{foo: _x}), do: true,def a(x) when is_integer(x), do: true,def a(_), do: false.À lire aussi : https://learnyouahaskell.github.io/syntax-in-functions.html/…
En Haskell, on peut appeler n’importe quelle fonction dans une garde, tandis qu’Erlang limite l’ensemble des fonctions autorisées à l’intérieur.