1 points par GN⁺ 3 시간 전 | 1 commentaires | Partager sur WhatsApp
  • Dans les gardes (guards) d’Elixir, le simple fait d’inverser des conditions avec or peut 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 renvoyant false, mais en échouant tout court, sans jamais atteindre la condition suivante
  • À cause de cette différence, Foo.a(%{foo: 21}), Foo.a(37) et Foo.b(%{foo: 21}) valent true, mais Foo.b(37) vaut false
  • On pourrait avoir l’impression que la commutativité des opérations booléennes est rompue, mais or avec 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 Foo définit deux fonctions, a/1 et b/1
    • a/1 : vérifie la garde dans l’ordre is_integer(x) or is_map_key(x, :foo)
    • b/1 : vérifie la garde dans l’ordre is_map_key(x, :foo) or is_integer(x)
    • Si la garde correspond, la fonction renvoie true, sinon la clause suivante renvoie false
  • a/1 : quand la condition sûre vient en premier

    • Foo.a(%{foo: 21}) vaut true
      • is_integer(x) vaut false
      • is_map_key(x, :foo) vaut true
      • Le résultat de or vaut true, donc la première clause correspond
    • Foo.a(37) vaut aussi true
      • is_integer(x) vaut true
      • Comme or fait 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 premier

    • Foo.b(%{foo: 21}) vaut true
      • is_map_key(x, :foo) vaut true
      • Le is_integer(x) suivant n’est pas exécuté
    • Foo.b(37) vaut false
      • La première condition, is_map_key(x, :foo), n’échoue pas en renvoyant false, elle échoue simplement
      • L’échec d’une seule fonction de garde n’est pas converti en false et fait échouer toute l’expression de garde
      • is_integer(x) n’est donc pas appelée, et la première clause ne correspond pas

É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 or fait 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

 
GN⁺ 3 시간 전
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) et dict: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/2 en 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ù or attrape les exceptions et les fusionne en false, ce serait sans doute encore plus surprenant dans d’autres cas.

  • 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.

    • C’est cette discussion qui m’a inspiré cet article.
  • 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_key n’implique pas un test is_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_key paraît un peu incohérent et donc surprenant ; il serait intéressant de vérifier, parmi les autres gardes is_..., lesquelles sont sûres et lesquelles doivent être évaluées avec une attente de type.

    • Je suis d’accord pour la référence à Pratchett, mais en ce moment il y a une canicule, et mon cerveau ne fonctionne pas comme prévu.
    • Par curiosité, j’en ai vérifié quelques-unes moi-même, et à première vue is_map_key semble être la seule garde is_ à exiger un type d’argument particulier.
      Les autres fonctions is_ ont un caractère booléen implicite et renvoient toujours true | false sans é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/…

    • Les gardes de Haskell sont un peu différentes.
      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.