Evaluación de cortocircuito en guards de Elixir: el orden de las condiciones cambia el resultado
(hauleth.dev)- En los guards de Elixir, con solo cambiar el orden de una condición con
or, el resultado de un código que parece la misma expresión lógica puede variar - En el orden
is_integer(x) or is_map_key(x,:foo), para una entrada entera la evaluación de cortocircuito ocurre primero y omite una comprobación riesgosa - En cambio,
is_map_key(x,:foo) or is_integer(x)hace que, con una entrada entera, la primera condición no devuelvafalsesino que falle, y no llegue a la siguiente condición - Por esta diferencia,
Foo.a(%{foo: 21}),Foo.a(37)yFoo.b(%{foo: 21})dantrue, peroFoo.b(37)dafalse - Puede parecer que se rompe la propiedad conmutativa de la operación booleana, pero
orcon cortocircuito depende del orden de las condiciones, y en Elixir 1.20.1 con OTP 29 no parece haber advertencia
Ejemplo donde el orden de las condiciones cambia el resultado
- El módulo de ejemplo
Foodefine dos funciones,a/1yb/1a/1: evalúa el guard en el ordenis_integer(x) or is_map_key(x, :foo)b/1: evalúa el guard en el ordenis_map_key(x, :foo) or is_integer(x)- Si el guard hace match, devuelve
true; si no, en la siguiente cláusula devuelvefalse
-
a/1: cuando la condición segura va primeroFoo.a(%{foo: 21})datrueis_integer(x)dafalseis_map_key(x, :foo)datrue- El resultado de
orestrue, así que hace match con la primera cláusula
Foo.a(37)también datrueis_integer(x)datrue- Como
orhace evaluación de cortocircuito,is_map_key(x, :foo)no se ejecuta
-
b/1: cuando la condición que puede fallar va primeroFoo.b(%{foo: 21})datrueis_map_key(x, :foo)datrueis_integer(x)no se ejecuta
Foo.b(37)dafalse- La primera condición,
is_map_key(x, :foo), en vez de devolverfalsefalla - El fallo de una sola función de guard no se convierte en
false, sino que hace fallar toda la expresión del guard is_integer(x)no se llama y la primera cláusula tampoco hace match
- La primera condición,
Evaluación de cortocircuito y ausencia de advertencias
- Para muchos desarrolladores de Elixir, este comportamiento puede parecer una ruptura de la propiedad conmutativa de los operadores booleanos
- Pero como
orhace evaluación de cortocircuito, no se puede asumir que cambiar el orden de las dos condiciones siempre dará el mismo resultado - El entorno de referencia es Elixir 1.20.1, OTP 29, y parece que Elixir no emite ninguna advertencia sobre este problema
1 comentarios
Comentarios en Lobste.rs
No soy programador de Elixir, pero lo más sorprendente del último ejemplo es que el error en la expresión de guardia no se propaga al llamador, sino que esa guardia se “omite”.
Creo entender por qué lo hicieron así, pero tampoco sorprende que produzca resultados contraintuitivos.
Es irónico si pensamos que el diseño de la API de Erlang buscaba ayudar a la programación intencional de la que habla Armstrong en su tesis sobre Erlang, p. 109/s4.5.
En la tesis se explican por separado funciones como
dict:fetch(Key, Dict),dict:search(Key, Dict)ydict:is_key(Key, Dict), que expresan la intención del programador: “la clave tiene que existir”, “puede existir, así que divido el flujo” y “solo reviso si existe”.Pero
is_map_key/2de Elixir lanza una excepción si el argumento “dict” no es un dict, y esa falla por excepción hace que falle toda la cláusula de guardia, lo que parece romper esa distinción.Por otro lado, si existiera un lenguaje donde
oratrapara excepciones y las combinara comofalse, quizá sería aún más sorprendente en otros casos.is_map_key/2en realidad es una función de Erlang completamente común.https://www.erlang.org/doc/apps/erts/erlang.html#is_map_key/2
Gracias a esta discusión que vi antes, estaba preparado para resolver este cuestionario, y en ese momento aprendí algunas cosas.
Aprendí algo, pero me da pena que hayan evitado la referencia a Pratchett.
Death debe de estar llevándose la mano a la frente en algún lugar.
Lo interesante aquí son dos cosas: no
false, sino una guardia fallida, hace fallar toda la expresión; y, de una forma algo contraintuitiva,is_map_keyno implica una comprobaciónis_map.Si se agrega una tercera variante como
is_map(x) and is_map_key(x, :corporal), funciona como se espera.El comportamiento de
is_map_keyparece un poco inconsistente y por eso resulta sorprendente; sería interesante revisar otros guardsis_...para ver cuáles son seguros y cuáles hay que evaluar asumiendo expectativas de tipo.is_map_keyes el único guardis_que exige un tipo específico de argumento.Las demás funciones
is_implican un comportamiento booleano y siempre devuelventrue | false, sin fallar.Aquí surge una pregunta interesante de estilo en Elixir.
El ejemplo es divertido y la explicación está bien, pero personalmente prefiero el pattern matching antes que las guardias siempre que sea posible.
Claro que hay excepciones, pero normalmente habría escrito funciones así con varias cláusulas:
def a(%{foo: _x}), do: true,def a(x) when is_integer(x), do: true,def a(_), do: false.También vale la pena ver: https://learnyouahaskell.github.io/syntax-in-functions.html/…
En Haskell se puede llamar cualquier función dentro de una guardia, pero Erlang limita el conjunto de funciones permitidas ahí.