Avaliação de curto-circuito em guards do Elixir: a ordem das condições muda o resultado
(hauleth.dev)- Nos guards do Elixir, apenas trocar a ordem de uma condição com
orjá 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 - Já
is_map_key(x,:foo) or is_integer(x)faz com que, para uma entrada inteira, a primeira condição não retornefalse, mas falhe, impedindo que a segunda condição seja avaliada - Por causa dessa diferença,
Foo.a(%{foo: 21}),Foo.a(37)eFoo.b(%{foo: 21})sãotrue, masFoo.b(37)éfalse - Pode parecer que a comutatividade das operações booleanas foi quebrada, mas
orcom 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
Foodefine duas funções,a/1eb/1a/1: verifica o guard na ordemis_integer(x) or is_map_key(x, :foo)b/1: verifica o guard na ordemis_map_key(x, :foo) or is_integer(x)- Se o guard casar, retorna
true; caso contrário, a cláusula seguinte retornafalse
-
a/1: quando a condição segura vem primeiroFoo.a(%{foo: 21})retornatrueis_integer(x)éfalseis_map_key(x, :foo)étrue- o resultado de
orétrue, então a primeira cláusula casa
Foo.a(37)também retornatrueis_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 primeiroFoo.b(%{foo: 21})retornatrueis_map_key(x, :foo)étrue- o
is_integer(x)seguinte não é executado
Foo.b(37)retornafalse- a primeira condição,
is_map_key(x, :foo), em vez de retornarfalse, 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
- a primeira condição,
Curto-circuito e ausência de aviso
- Para muitos desenvolvedores Elixir, esse comportamento pode parecer uma quebra da comutatividade dos operadores booleanos
- Mas
orfaz 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
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/2do 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
orcapturasse exceções e as combinasse comofalse, isso talvez fosse ainda mais surpreendente em outros casos.is_map_key/2é, na verdade, uma função Erlang completamente comum.https://www.erlang.org/doc/apps/erts/erlang.html#is_map_key/2
Graças a esta discussão que vi antes, eu estava preparado para resolver este quiz, e aprendi algumas coisas naquela ocasião.
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_keynão inclui implicitamente uma verificaçãois_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_keyparece um pouco inconsistente e por isso soa surpreendente; também seria interessante verificar, entre os outros guardsis_..., quais são seguros e quais precisam ser avaliados assumindo uma expectativa de tipo.is_map_keyé o único guardis_que exige um tipo específico de argumento.As outras funções
is_têm caráter booleano implícito e sempre retornamtrue | 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/…
Em Haskell é possível chamar funções arbitrárias dentro de um guard, enquanto Erlang restringe o conjunto de funções permitidas ali.