1 pontos por GN⁺ 3 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • Nos guards do Elixir, apenas trocar a ordem de uma condição com or já pode mudar o resultado de um código que parece ter a mesma lógica
  • Na ordem is_integer(x) or is_map_key(x,:foo), para uma entrada inteira, a avaliação de curto-circuito acontece primeiro e pula a verificação perigosa
  • is_map_key(x,:foo) or is_integer(x) faz com que, para uma entrada inteira, a primeira condição não retorne false, mas falhe, impedindo que a segunda condição seja avaliada
  • Por causa dessa diferença, Foo.a(%{foo: 21}), Foo.a(37) e Foo.b(%{foo: 21}) são true, mas Foo.b(37) é false
  • Pode parecer que a comutatividade das operações booleanas foi quebrada, mas or com curto-circuito naturalmente depende da ordem das condições, e não há aviso no Elixir 1.20.1 com OTP 29

Exemplo em que a ordem das condições muda o resultado

  • O módulo de exemplo Foo define duas funções, a/1 e b/1
    • a/1: verifica o guard na ordem is_integer(x) or is_map_key(x, :foo)
    • b/1: verifica o guard na ordem is_map_key(x, :foo) or is_integer(x)
    • Se o guard casar, retorna true; caso contrário, a cláusula seguinte retorna false
  • a/1: quando a condição segura vem primeiro

    • Foo.a(%{foo: 21}) retorna true
      • is_integer(x) é false
      • is_map_key(x, :foo) é true
      • o resultado de or é true, então a primeira cláusula casa
    • Foo.a(37) também retorna true
      • is_integer(x) é true
      • como há avaliação de curto-circuito, is_map_key(x, :foo) não é executado
  • b/1: quando a condição que pode falhar vem primeiro

    • Foo.b(%{foo: 21}) retorna true
      • is_map_key(x, :foo) é true
      • o is_integer(x) seguinte não é executado
    • Foo.b(37) retorna false
      • a primeira condição, is_map_key(x, :foo), em vez de retornar false, falha
      • a falha de uma função de guard não é convertida em false, mas faz toda a expressão do guard falhar
      • is_integer(x) não é chamado, e a primeira cláusula também não casa

Curto-circuito e ausência de aviso

  • Para muitos desenvolvedores Elixir, esse comportamento pode parecer uma quebra da comutatividade dos operadores booleanos
  • Mas or faz avaliação de curto-circuito, então não dá para assumir que trocar a posição das duas condições sempre produzirá o mesmo resultado
  • O ambiente de referência é Elixir 1.20.1, OTP 29, e aparentemente o Elixir não emite aviso para esse caso

1 comentários

 
GN⁺ 3 시간 전
Comentários no Lobste.rs
  • Não sou programador Elixir, mas o mais surpreendente no último exemplo é que erros em expressões de guarda não são propagados para o chamador; aquela guarda é “pulada”.
    Acho que entendo por que fizeram assim, mas também não surpreende que isso gere resultados contraintuitivos.

  • É irônico quando se considera que o design das APIs do Erlang foi pensado para ajudar na programação intencional de que Armstrong fala na tese sobre Erlang, p. 109/s. 4.5.
    A tese explica a separação entre funções como dict:fetch(Key, Dict), dict:search(Key, Dict), dict:is_key(Key, Dict), que expressam a intenção do programador: “a chave necessariamente deve existir”, “ela pode existir, então bifurco o fluxo” e “só verifico se existe”.
    Mas is_map_key/2 do Elixir lança uma exceção se o argumento “dict” não for um dict, e essa falha por exceção leva à falha de toda a cláusula de guarda, o que parece quebrar essa distinção.
    Por outro lado, se houvesse uma linguagem em que or capturasse exceções e as combinasse como false, isso talvez fosse ainda mais surpreendente em outros casos.

  • Graças a esta discussão que vi antes, eu estava preparado para resolver este quiz, e aprendi algumas coisas naquela ocasião.

    • Foi inspirado por aquela discussão que escrevi este texto.
  • Aprendi algo, mas fiquei com pena de terem evitado uma referência a Pratchett.
    Imagino a Morte em algum lugar levando a mão à testa.
    O interessante aqui são duas coisas: uma guarda que falha, e não false, faz a expressão inteira falhar; e, de modo um tanto contraintuitivo, is_map_key não inclui implicitamente uma verificação is_map.
    Se você adicionar uma terceira variante como is_map(x) and is_map_key(x, :corporal), ela se comporta como esperado.
    O comportamento de is_map_key parece um pouco inconsistente e por isso soa surpreendente; também seria interessante verificar, entre os outros guards is_..., quais são seguros e quais precisam ser avaliados assumindo uma expectativa de tipo.

    • Concordo com a referência a Pratchett, mas agora está uma onda de calor, então meu cérebro não está funcionando como esperado.
    • Fiquei curioso e conferi algumas coisas por conta própria; à primeira vista, parece que is_map_key é o único guard is_ que exige um tipo específico de argumento.
      As outras funções is_ têm caráter booleano implícito e sempre retornam true | false, sem falhar.
  • Isso levanta uma questão interessante sobre estilo em Elixir.
    Os exemplos são divertidos e a explicação é boa, mas, pessoalmente, prefiro pattern matching a guards sempre que possível.
    Claro que há exceções, mas eu provavelmente escreveria funções assim com várias cláusulas, como def a(%{foo: _x}), do: true, def a(x) when is_integer(x), do: true, def a(_), do: false.

  • Vale ver também: https://learnyouahaskell.github.io/syntax-in-functions.html/…

    • Os guards de Haskell são um pouco diferentes.
      Em Haskell é possível chamar funções arbitrárias dentro de um guard, enquanto Erlang restringe o conjunto de funções permitidas ali.