Antes de partir para a resolução de alguns exercícios, achei válido pontuar mais algumas funcionalidades do Elixir em um “apanhadão”. Neste post abordo sobre estruturas de controle, funções e operador Pipe, além de rever um pouco mais sobre pattern matching.
Obs: Me senti bem idiota depois de escrever esta parte. No final das contas ficou bem próximo
ao tutorial oficial, disponível em https://elixir-lang.org/getting-started/case-cond-and-if.html
e em https://elixirschool.com/en/lessons/basics/control-structures/, exceto a
parte do with
, que dei uma atenção maior. Então sinta-se à vontade em pular para o próximo
tópico. Pelo menos me serviu pra fixar o conteúdo, que inclusive é o proposito disso aqui.
Uma breve explicação das estruturas case
, cond
, if/else/unless
, with
e
blocos com do/end
.
case
Trata-se de um switch-case, no qual podemos simplificar múltiplos if/else. Quando uma condição
é satisfatória, seu resultado é devolvido e as demais condições do bloco não são averiguadas.
Repere que quando fazemos case
com uma lista, não à percorremos item por item, mas
aplicamos cada condição nela inteira.
case {1, 2, 3} do
{1} ->
"Esta condição não será correspondida."
{1, 2, 3} ->
"Esta condição será correspondida."
end
"Esta condição será correspondida."
case {1, 2, 3} do
{2, 3, 4} ->
"Esta condição não será correspondida."
{1, x, 3} ->
"Esta condição será correspondida e
atribuirá 'x' à #{x}."
end
"Esta condição será correspondida e\n atribuirá 'x' à 2."
Obs: Cuidado com o escopo, no segundo exemplo a variável x = 2
é atribuída e
disponibilizada dentro da condição dela. Se, por exemplo, você fizer x = 7
antes do case
,
depois dele verá que x ainda vale 7, ou seja, não é reescrito.
Você pode usar o operador ^
antes de uma variável para fazer a condição sob ela. Para
definir uma condição default, que sempre será válida, basta utilizar _
.
x = 1
case 2 do
^x -> "não irá corresponder."
_ -> "irá corresponder."
end
"irá corresponder."
Note que _
deve ser a última se não o case
sempre cairá nela. Outro
detalhe é que se nenhuma condição for satisfeita é obtido o erro CaseClauseError.
case 2 do
_ -> "irá corresponder."
^x -> "Também corresponde, mas não chegará até aqui."
end
"irá corresponder."
case :fool do
:smart -> "não irá corresponder."
end
** (CaseClauseError) no case clause matching: :fool
cond
Parecido com o case
, mas atende a necessidade de checar múltiplos valores ou condições para
além de igualdade.
cond do
1 + 1 == 3 ->
"é falso."
2 - 1 == 3 ->
"também é falso."
1 + 2 == 3 ->
"é verdadeiro."
end
"é verdadeiro."
Importante pontuar que tudo para além de nil
e false
é considerado verdadeiro.
iex> hd(["a", "b", "c"])
"a"
cond do
hd(["a", "b", "c"]) ->
"'a' é considerado como verdadeiro."
end
"'a' é considerado como verdadeiro."
if e unless
Nada de novo, são basicamente iguais a maioria das linguagens. Ambos aceitam o bloco else
e unless
checa o inverso de if
. Lembrando, novamente, que tudo para além de nil
e false
é considerado verdadeiro.
iex> if true do
...> "isto será devolvido!"
...> end
"isto será devolvido!"
iex> unless true do
...> "isto nunca será devolvido!"
...> end
nil
iex> if nil do
...> "isto não será devolvido."
...> else
...> "mas isto será."
...> end
"mas isto será."
iex> unless 1 do
...> "isto não será devolvido, '1' é considerado true."
...> else
...> "mas isto será."
...> end
"mas isto será."
Blocos do/end
Provavelmente você já deve ter notado que funções e estruturas de controle são delimitadas por
do
e end
. Nada de novo também, mas vale pontuar que é possível simplificar esse blocos
em uma linha:
iex> if true, do: 1 + 2
3
iex> if false, do: :this, else: :that
:that
with
Introduzido na versão 1.2 do Elixir, a estrutura with
permite simplificar o código,
substituído, por exemplo, clausulas aninhadas de case
.
full_name = %{first: "Nila", last: "Minha gatatinha idosa"}
with {:ok, first} <- Map.fetch(full_name, :first),
{:ok, last} <- Map.fetch(full_name, :last),
do: last <> ", " <> first
"Minha gatatinha idosa, Nila"
only_first = %{first: "Nila"}
with {:ok, first} <- Map.fetch(only_first, :first),
{:ok, last} <- Map.fetch(only_first, :last),
do: last <> ", " <> first
:error
Primeiro, vamos entender precisamente este exemplo. O que faz a função fetch/2
do módulo
Map
? Ela “pega” o valor de uma chave de um map:
iex> m = %{a: 1, b: 3, c: 5}
iex> Map.fetch(m, :a)
{:ok, 1}
iex> Map.fetch(m, :b)
{:ok, 3}
iex> Map.fetch(m, :d)
:error
Note que seu retorno não é apenas o valor, mas uma tuple com o atom :ok
mais o valor da
chave requisitada. Repare ainda que ao não encontrar devolve :error
sozinho.
Para fazer o equivalente com case
, perceba como aumenta a verbosidade do código:
full_name = %{first: "Nila", last: "Minha gatatinha idosa"}
case Map.fetch(full_name, :first) do
{:ok, first} ->
case Map.fetch(full_name, :last) do
{:ok, last} -> last <> ", " <> first
:error -> :error # retorna o atom :error
end
:error -> :error # retorna o atom :error
end
"Minha gatatinha idosa, Nila"
only_first = %{first: "Nila"}
case Map.fetch(only_first, :first) do
{:ok, first} ->
case Map.fetch(only_first, :last) do
{:ok, last} -> last <> ", " <> first
:error -> :error # retorna o atom :error
end
:error -> :error # retorna o atom :error
end
:error
Obs: Note como a direção das flechas apontam a "direção de partida" da execução e, consequentemente, leitura do código:
->
em case
, significa que se o conteúdo à esquerda da flecha der match com o argumento do case
, executamos então o que segue a direita dela.<-
em with
, significa que se o conteúdo à direita da flecha der match com o da esquerda, executamos então o bloco do
.Para esclarecer um pouco mais este poder de síntese com with
, vamos para outro exemplo.
Suponha o seguinte trecho de código encontrado em um projeto Elixir:
case Repo.insert(changeset) do
{:ok, user} ->
case Guardian.encode_and_sign(user, :token, claims) do
{:ok, token, full_claims} ->
important_stuff(token, full_claims)
error ->
error
end
error ->
error
end
Mesmo não sabendo o comportamento, e muito menos como foi implementado, algumas das chamadas feitas neste trecho, podemos interpretar que:
case
, se Repo.insert(changeset)
retornar um {:ok, user}
entramos
no segundo case
, caso contrário obtemos um erro.case
, se Guardian.encode_and_sign(user, :token, claims)
retornar um
{:ok, token, full_claims}
então a chamada de important_stuff(token, full_claims)
é
feita, caso contrário obtemos o mesmo erro anterior.Note como podemos simplificar, utilizando menos linhas, mas mantendo a lógica e até aumentando a legibilidade.
with {:ok, user} <- Repo.insert(changeset),
{:ok, token, full_claims} <- Guardian.encode_and_sign(user, :token, claims) do
important_stuff(token, full_claims)
end
with com else
A partir da versão 1.3, with/1
também suporta else
. Vejamos primeiro um exemplo com
with
direto (não aninhado):
full_name = %{first: "Nila", last: "Minha gatatinha idosa"}
with {:ok, first} <- Map.fetch(full_name, :first) do
"O primeiro nome é '#{first}'"
else
:error -> "Erro! Não há a chave ':first'"
end
"O primeiro nome é 'Nila'"
only_last = %{last: "Minha gatatinha idosa"}
with {:ok, first} <- Map.fetch(only_last, :first) do
"O primeiro nome é '#{first}'"
else
:error -> "Erro! Não há a chave ':first'"
end
"Erro! Não há a chave ':first'"
Agora refazendo o primeiro exemplo, aninhado com dois matchs, temos:
full_name = %{first: "Nila", last: "Minha gatatinha idosa"}
with {:ok, first} <- Map.fetch(full_name, :first),
{:ok, last} <- Map.fetch(full_name, :last) do
last <> ", " <> first
else
:error -> "Erro! precisa ter as chaves 'first' e 'last'"
end
"Minha gatatinha idosa, Nila"
only_first = %{first: "Nila"}
with {:ok, first} <- Map.fetch(only_first, :first),
{:ok, last} <- Map.fetch(only_first, :last) do
last <> ", " <> first
else
:error -> "Erro! precisa ter as chaves 'first' e 'last'"
end
"Erro! precisa ter as chaves 'first' e 'last'"
only_last = %{last: "Minha gatatinha idosa"}
with {:ok, first} <- Map.fetch(only_last, :first),
{:ok, last} <- Map.fetch(only_last, :last) do
last <> ", " <> first
else
:error -> "Erro! precisa ter as chaves 'first' e 'last'"
end
"Erro! precisa ter as chaves 'first' e 'last'"
Como Map.fetch
é chamado em ambos os matchs, o bloco de else
precisa lidar apenas
com um único caso de negativa - quando Map.fetch
retorna :error
. Como lidar então com
múltiplos casos de negativa?
defmodule Fool do
import Integer
def check_even_on_map(m, key) do
with {:ok, number} <- Map.fetch(m, key),
true <- is_even(number) do
"é par"
else
:error ->
"Valor não encontrado no map"
false ->
"é impar"
end
end
end
iex> my_map = %{a: 2, b: 3}
iex> Fool.check_even_on_map(my_map, :a)
"é par"
iex> Fool.check_even_on_map(my_map, :b)
"é impar"
iex> Fool.check_even_on_map(my_map, :c)
"Valor não encontrado no map"
Temos um with
com dois casos distintos de negativa tratados pelo bloco else
:
Map.fetch
, que retorna :error
is_even/1
, do módulo Integer
importado, que retorna false
.Síntese sobre função anonima, função nomeada e pattern matching em funções.
Funções Anonimas
Definidas entre os termos fn
e end
, funções anonimas podem ter qualquer número de
argumentos e múltiplos blocos de execução separados com ->
, sendo à esquerda da flecha os
argumentos de um bloco e a direita o bloco desses argumentos.
iex> sum = fn (a, b) -> a + b end
iex> sum.(3, 7)
10
iex> sum.(3, -1)
2
iex> multi = fn (a, b) -> a * b end
iex> multi.(3, 2)
6
iex> multi.(-3, 3)
-9
Existem ainda uma abreviação com uso de &1
, &2
, &3
etc, para os argumentos da
função anonima.
iex> sum = &(&1 + &2)
iex> sum.(7, 7)
14
iex> triple_concat = &(&1 <> &2 <> &3)
iex> triple_concat.("Olá", ", ", "mundo")
"Olá, mundo"
Pattern Matching em Funções Anonimas
Justamente para utilizar múltiplos blocos de execução, ou seja, fazer diferentes execuções, utilizamos pattern matching sob os argumentos de uma função anonima.
handle_result = fn
{:ok, result} -> "Sucesso! mensagem: #{result}"
{:error} -> "Erro! Consulte o administrador"
end
iex> handle_result.({:ok, :uploaded})
"Sucesso! mensagem: uploaded"
iex> handle_result.({:error})
"Erro! Consulte o administrador"
Isso pode ser ainda mais estendido com cláusulas guard através do termo when
.
handle_result = fn
{:ok, result} when is_nil(result) ->
"Sucesso! Sem resposta do servidor"
{:ok, result} when is_number(result) ->
"Sucesso! código: #{result}"
{:ok, result} ->
"Sucesso! mensagem: #{result}"
{:error} ->
"Erro! Consulte o administrador"
end
iex> handle_result.({:ok, nil})
"Sucesso! Sem resposta do servidor"
iex> handle_result.({:ok, 2345})
"Sucesso! código: 2345"
iex> handle_result.({:ok, :logout})
"Sucesso! mensagem: logout"
iex> handle_result.({:error})
"Erro! Consulte o administrador"
Um cuidado que se dever ter com guards é que caso tenha um bloco com um conjunto de argumentos
direto, sem guard, os blocos com este mesmo conjunto de argumentos que tenham guards devem
vir antes do primeiro. Por exemplo, se no trecho anterior, o bloco
{:ok, result} -> "Sucesso! mensagem: #{result}"
fosse o primeiro, note que os blocos
seguintes são ignorados.
bad_handle = fn
{:ok, result} ->
"Sucesso! mensagem: #{result}"
{:ok, result} when is_nil(result) ->
"Sucesso! Sem resposta do servidor"
{:ok, result} when is_number(result) ->
"Sucesso! código: #{result}"
{:error} ->
"Erro! Consulte o administrador"
end
iex> bad_handle.({:ok, nil})
"Sucesso! mensagem: "
iex> bad_handle.({:ok, 2345})
"Sucesso! mensagem: 2345"
iex> bad_handle.({:ok, :logout})
"Sucesso! mensagem: logout"
Funções Nomeadas
Como já vimos, funções nomeadas são definidas com o termo def
dentro de um módulo. Seu bloco
é delimitado por do/end
, com a possibilidade de abreviação em uma linha com do:
.
defmodule Greeter do
def hello(name) do
"Hello, " <> name
end
def same_hello(name), do: "Hello, " <> name
end
iex> Greeter.hello("Nila")
"Hello, Nila"
iex> Greeter.same_hello("Nila")
"Hello, Nila"
Lembrando também, que as funções são distinguidas pelo Elixir por seu nome + aridade (quantidade de argumentos).
defmodule Greeter2 do
def hello(), do: "Olá desconhecido" # hello/0
def hello(name), do: "Olá, " <> name # hello/1
def hello(n1, n2), do: "Olá, #{n1} e #{n2}" # hello/2
end
Pattern Matching em Funções Nomeadas
Dado último post, Preciso falar sobre “Pattern Matching”, vamos complicar um pouco somando pattern matching e recursão.
defmodule Length do
def of([]), do: 0
def of([_ | tail]), do: 1 + of(tail)
end
iex> Length.of []
0
iex> Length.of [1]
1
iex> Length.of [1, 2]
2
iex> Length.of [1, 2, 3]
3
iex> Length.of [1, 2, 3, 4]
4
O laço de recursão de of
gira em torno do pattern matching. Temos que se o argumento:
[]
, encerra o laço e devolve o valor 0.of
da calda do argumento (segue o laço)Por isso Length.of/1
devolve o tamanho da lista.
Note ainda que o pattern matching não esta apenas na primeira parte, em que []
é checado,
mas como também esta na segunda parte, em que tail
é associado a calda do parâmetro. É
possível fazer a mesma recursão, substituído o segundo match pelo método tl/1
.
defmodule Length do
def of([]), do: 0
def of(list), do: 1 + of(tl(list))
end
Da mesma forma como vimos anteriormente com funções anonimas, também podemos usar guards.
defmodule Length do
def of([]), do: 0
def of([_ | tail]), do: 1 + of(tail)
def of(not_a_list) when not is_list(not_a_list) do
"Erro! O argumento precisa ser uma lista"
end
end
iex> Length.of "ops!"
"Erro! O argumento precisa ser uma lista"
iex> Length.of [3, 4, 5]
3
Importante relembrar que o pattern matching é feito para ambos os lados de =
, dentro da
definição do argumento
defmodule Greeter do
def hello(%{name: person_name} = person) do
IO.puts "Olá, #{person_name}"
IO.inspect person
end
def hello(%{first: first_name} = %{last: last_name} = person) do
IO.puts "Olá, #{first_name} #{last_name}"
IO.inspect person
end
end
iex> my_cat = %{name: "Nila", age: "16", favorite_hobby: "sleep"}
iex> Greeter.hello my_cat
Olá, Nila
%{age: "16", favorite_hobby: "sleep", name: "Nila"}
iex> my_self = %{first: "Eugenio Augusto", last: "Jimenes", age: "16", favorite_hobby: "sleep"}
iex> Greeter.hello my_self
Olá, Eugenio Augusto Jimenes
%{age: "16", favorite_hobby: "sleep", first: "Eugenio Augusto", last: "Jimenes"}
Representado por |>
, ele passa o resultado da expressão à sua esquerda para “o que vier” à
sua direita.
iex> "Minha gata é velhinha" |> String.split()
["Minha", "gata", "é", "velhinha"]
iex> "Minha gata é velhinha" |> String.upcase() |> String.split()
["MINHA", "GATA", "É", "VELHINHA"]
Quando lidamos com funções que possuem mais de um argumento o resultado que vem do pipe (à
esquerda) entrará sempre como o primeiro parâmetro (à direita). Por exemplo, a própria função
String.split
possui um segundo argumento opcional.
iex> "Minha_gata_é_velhinha" |> String.split("_")
["Minha", "gata", "é", "velhinha"]
Note que sempre entrará como primeiro parâmetro.
iex> "_" |> String.split("Minha_gata_é_velhinha")
["_"]
iex> String.split("_", "Minha_gata_é_velhinha")
["_"]
iex> String.split("Minha_gata_é_velhinha", "_")
["Minha", "gata", "é", "velhinha"]
Esse operador é extremamente presente nos códigos em Elixir. Com ele é possível escrever lógicas consecutivas, logo sucintas e de fácil leitura.
defmodule Greeter do
def hello(names) when is_list(names) do
names
|> Enum.join(", ")
|> hello
end
def hello(name) when is_binary(name) do
phrase() <> name
end
defp phrase, do: "Olá, "
end
iex> Greeter.hello "Eugenio"
"Olá, Eugenio"
iex> Greeter.hello ["Eugenio", "Lucas", "Tomaz"]
"Olá, Eugenio, Lucas, Tomaz"
Por agora, é isso.