
Você sabia que tanto Erlang como Elixir não possuem um operador de atribuição? Ou seja, o que significa a seguinte expressão abaixo:
1message = "Hello World"
Mais conhecido como Pattern Matching, esse mecanismo te permite fazer associações de valores através da comparação de símbolos entre o lado esquerdo do operador e o lado direito.
Na prática:
1iex> hello_world = "Hello World" 2iex> hello_world 3> "Hello World" 4 5iex> "Hello " <> world = "Hello World" 6iex> world 7> "World"
Os 2 exemplos acima ilustram o funcionamento do casamento de padrões. Onde a primeira execução aparenta ser uma simples atribuição, mas a partir da segunda, é possível perceber uma grande diferença conceitual.
No segundo exemplo a runtime irá tentar encontrar o valor "Hello " no valor do lado direito do operador, se esse valor for encontrado(deu match)
o resto do valor será armazenado na variável world.d
Quando essa assertiva não acontece, e o padrão não é encontrado, uma exception é gerada.
1iex> "Hellu " <> world = "Hello World" 2> ** (MatchError) no match of right hand side value: "Hello World"
Essa é uma das minhas funcionalidades favoritas no ecossistema Erlang/Elixir, e neste texto vou tentar sintetizar um pouco do porquê.
Ex-1. Em Elixir, o casamento de padrões pode ser utilizado em Strings:
1# Apenas uma comparação e nada acontece, pois o padrão é encontrado (caso contrário, geraria uma exception) 2"Hello World" = "Hello World" 3 4# Assimila o valor "World" à variável world 5"Hello " <> world = "Hello World" 6 7# Mesmo resultado do exemplo acima, porém escrito de uma forma diferente. (usando operadores de bitstring) 8<<"Hello ", world::binary>> = "Hello World" 9 10# Assimila os primeiros 5 caracteres na variável hello, valida um espaço " ", e joga o resto da string na variável world 11<<hello::binary-size(5), " ", world::binary>> = "Hello World" 12 13# Como não existe nenhum padrão a ser encontrado, meio que simula uma atribuição. 14hello_world = "Hello World"
É possível usar casamento de padrões com praticamente todos tipos, strings, listas, tuplas, maps, structs...
Ex-2. Testando outros tipos:
1# Apenas procura o padrão do lado esquerdo no lado direito do operador, o mesmo com as strings. 2[1, 2, 3] = [1, 2, 3] 3 4# Assimila o valor 2 na variável second. 5[1, second, 3] = [1, 2, 3] 6 7# Assimila o valor 1 na variável first, e descarta os demais valores. (operadores importantes que usamos para listas `++` e `|`) 8[first | _] = [1, 2, 3] 9 10# Ignora apenas o primeiro valor da lista, e assimila os demais na variável rest. (2, 3) 11[_ | rest] = [1, 2, 3] 12 13# Simplesmente joga todos os valores da direita do operador na variável list. 14list = [1, 2, 3]
E o quão relevante é este tipo de operação no dia-a-dia? Casamento de padrões pode ser utilizado em praticamente tudo, validação, polimorfia de funções, estruturas de controle, funções de ordem maior.
Ex-3. Em Elixir costumamos trabalhar bastante com o conceito de Tuplas de erro ou sucesso, e isso vale para requests, comunicação externa, funções impuras, e por ai adiante...
Digamos que a execução de uma função qualquer do nosso código irá retornar uma tupla de sucesso, contendo {status, person}
Podemos tirar vantagem do casamento de padrões nesse cenário para criar uma tomada de decisão no nosso código.
Ilustração:
1{:ok, %{name: "Willian"}} = {:ok, %{name: "Willian"}} 2 3# Assimila o valor "Willian" na variável name. 4{:ok, %{name: name}} = {:ok, %{name: "Willian"}} 5 6# Assimila todo o map da pessoa na variável person 7{:ok, person} = {:ok, %{name: "Willian"}} 8 9# Assimila :ok no status e o map pessoa na variável person 10{status, person} = {:ok, %{name: "Willian"}} 11 12# Joga todo o valor da direita do operador na variável tuple_result 13tuple_result = {:ok, %{name: "Willian"}}
Criando uma tomada de decisão a partir desse valor:
1defmodule Person do 2 def print_name(result) do 3 case result do 4 {:ok, %{name: name}} -> 5 IO.puts(name) 6 7 _ -> 8 IO.puts("Houve um erro ao processar o resultado") 9 end 10 end 11end 12 13# se executarmos este código passando nosso resultado do exemplo anterior teríamos algo como: 14iex> Person.print_name({:ok, %{name: "Willian"}}) 15> "Willian"
Agora com polimorfismo:
1defmodule Person do 2 def print_name({:ok, %{name: name}}), 3 do: IO.puts(name) 4 5 def print_name(_), 6 do: IO.puts("Houve um erro ao processar o resultado") 7end
Em funções de ordem maior:
1list_results = [ 2 {:ok, %{name: "Willian"}}, 3 {:ok, %{name: "Willian"}}, 4 {:error, "Algo deu ruim"}, 5 {:ok, %{name: "Willian"}}, 6 {:ok, %{name: "Willian"}}, 7 {:ok, %{name: "Willian"}}, 8 {:error, "Algo deu ruim"} 9] 10 11res = Enum.filter(list_results, fn 12 {:ok, %{name: name}} -> true 13 {:error, _reason} -> false 14end)
No caso acima, iremos filtrar da lista de resultados somente as tuplas de sucesso, e ignorar as que tiveram erro.
Concluindo, essa funcionalidade me permiti flexibilidade na hora de manipular dados e basicamente escrever toda estrutura do meu código, tratamento de erros, estruturas condicionais, polimorfismo, e muito mais que não consigo nem lembrar direito.