1 puntos por GN⁺ 3 시간 전 | 1 comentarios | Compartir por WhatsApp
  • 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 devuelva false sino que falle, y no llegue a la siguiente condición
  • Por esta diferencia, Foo.a(%{foo: 21}), Foo.a(37) y Foo.b(%{foo: 21}) dan true, pero Foo.b(37) da false
  • Puede parecer que se rompe la propiedad conmutativa de la operación booleana, pero or con 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 Foo define dos funciones, a/1 y b/1
    • a/1: evalúa el guard en el orden is_integer(x) or is_map_key(x, :foo)
    • b/1: evalúa el guard en el orden is_map_key(x, :foo) or is_integer(x)
    • Si el guard hace match, devuelve true; si no, en la siguiente cláusula devuelve false
  • a/1: cuando la condición segura va primero

    • Foo.a(%{foo: 21}) da true
      • is_integer(x) da false
      • is_map_key(x, :foo) da true
      • El resultado de or es true, así que hace match con la primera cláusula
    • Foo.a(37) también da true
      • is_integer(x) da true
      • Como or hace evaluación de cortocircuito, is_map_key(x, :foo) no se ejecuta
  • b/1: cuando la condición que puede fallar va primero

    • Foo.b(%{foo: 21}) da true
      • is_map_key(x, :foo) da true
      • is_integer(x) no se ejecuta
    • Foo.b(37) da false
      • La primera condición, is_map_key(x, :foo), en vez de devolver false falla
      • 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

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 or hace 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

 
GN⁺ 3 시간 전
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) y dict: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/2 de 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 or atrapara excepciones y las combinara como false, quizá sería aún más sorprendente en otros casos.

  • Gracias a esta discusión que vi antes, estaba preparado para resolver este cuestionario, y en ese momento aprendí algunas cosas.

    • Esa discusión me inspiró a escribir este artículo.
  • 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_key no implica una comprobación is_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_key parece un poco inconsistente y por eso resulta sorprendente; sería interesante revisar otros guards is_... para ver cuáles son seguros y cuáles hay que evaluar asumiendo expectativas de tipo.

    • Coincido con lo de la referencia a Pratchett, pero ahora mismo hay una ola de calor, así que mi cerebro no está funcionando como se esperaba.
    • Me dio curiosidad y revisé algunas cosas por mi cuenta; a grandes rasgos, parece que is_map_key es el único guard is_ que exige un tipo específico de argumento.
      Las demás funciones is_ implican un comportamiento booleano y siempre devuelven true | 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/…

    • Las guardias de Haskell son un poco distintas.
      En Haskell se puede llamar cualquier función dentro de una guardia, pero Erlang limita el conjunto de funciones permitidas ahí.