Elixir no dia a dia - Pattern Matching

girorme 9 min read March 15, 2022 1766 words
Pattern matching é uma poderosa parte de Elixir que nos permite procurar padrões simples em valores, estruturas de dados, e até funções

Pattern matching é uma poderosa parte de Elixir que nos permite procurar padrões simples em valores, estruturas de dados, e até funções

Agenda

leafs

ref: https://elixirschool.com/pt/lessons/basics/pattern_matching

O Básico

O operador = no elixir é tratado de forma difente comparado a outras linguagens. Chamamos de match operator esse carinha que além de extrair valores, pode até ser usado como “substituto” de estruturas de condições em alguns casos, além de armazenar valores.

Então tenha em mente que quando usamos o operador, estamos fazendo um match: se a operação do lado esquerdo dá match com a do lado direito, nós temos uma operação válida.

tom-confuse

pra descomplicar… ou complicar mais

Isso quer dizer que, podemos fazer “comparações” e armazenar valores:

1
2
3
4
5
1 = 1
"rodrigo" = "rodrigo"

num = 1 
nome = "rodrigo"

1 - As duas primeiras expressões são válidas pq literalmente: 1 é igual a 1 e a string “rodrigo” é igual a string “rodrigo”

2 - Nas expressões seguintes, o match operator atua como um “bindador” de valores, já que não temos um valor literal do lado esquerdo e sim uma varíavel, então o elixir sabe que deve associar o valor do lado direito ao esquerdo

Extração de valores

Para fazer um paralelo, vale lembrar de funcionalidades de outras linguagem como o Destructuring do js ou até msm o list do php (nas novas versões do php tbm temos um operador match: match php) para depois voltarmos ao pattern matching:

1
2
3
4
5
6
7
8
9
let arr = ["John", "Smith"]

// destructuring assignment
// sets firstName = arr[0]
// and surname = arr[1]
let [firstName, surname] = arr;

alert(firstName); // John
alert(surname);  // Smith

List do php:

1
2
3
list($nome, $idade) = ["Rodrigo", 30];
echo $nome; // "Rodrigo"
echo $idade; // 30

Por outro lado com o match operator podemos fazer extrações com qualquer tipo de dado da linguagem, vamos aos exemplos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[first_n, second] = [1, 2] 

# first_n => 1
# second => 2

[1, second] = [1, 2] # podemos fazer match + extrações de forma bem simples

# second => 2

%{first_name: name, extra_info: info} = %{first_name: "foo", extra_info: "bar"}

# name => "foo"
# extra_info => "bar"

Além de fazer extrações temos um cenário interessante no seguinte trecho:

1
[1, second] = [1, 2]

Como estamos fazendo um match, a expressão é valida pq além do tipo do dado ser compatível (lista) temos o primeiro valor igual dos dois lados.

Podemos validar que os lados são comparados se passarmos valores diferentes, veja:

1
2
3
4
5
iex(1)> [1, second] = [1, 2]
[1, 2] # ok

iex(2)> [2, second] = [1, 2]
** (MatchError) no match of right hand side value: [1, 2]

Na primeira expressão, ok, temos o número 1 do lado direito e do esquerdo. Na segunda temos um erro de match porque os valores não condizem dos dois lados.

Vale lembrar que o erro de match irá ocorrer literalmente em qualquer falha de comparação:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
iex(1)> %{name: "Rodrigo", year: 2022} = %{name: "Rodrigo", year: 2022}

%{name: "Rodrigo", year: 2022}
# ok,
# aqui estamos apenas comparando os valores

iex(2)> %{name: name, year: 2022} = %{name: "Rodrigo", year: 2022}
# ok, estamos armazendo a string "Rodrigo" dentro da var name
# name => "Rodrigo"

iex(3)> %{name: name, year: 2021} = %{name: "Rodrigo", year: 2022}
# erro
** (MatchError) no match of right hand side value: %{name: "Rodrigo", year: 2022}

na segunda expressão, conseguimos armazenar o valor “Rodrigo” na variável name mas na terceira não é possível, já que do lado direito temos o ano 2022 e do lado esquerdo temos o valor 2021.

Pattern matching em tudo!!!

Na medida que nos habituamos com a linguagem percebemos que patterning matching está presente em muitas operações, porque a legibilidade do código aumenta e deixamos nossas intenções mais claras:

Funções

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
defmodule UserUtils do
  def extract_name(%{name: name_var}) do
    IO.puts "Name: #{name_var}"
  end
end

# chamada da função:
user = %{name: "Rodrigo", year: 2022}
UserUtils.extract_name(user)

# resultado:
# Name: Rodrigo

É válido observar e já introduzir um ponto importante: podemos fazer matches parciais em mapas. Veja que definimos um mapa com 2 chaves: name e year mas na função extract_name conseguimos dar match somente na chave name. Nós literalmente falamos:

  • função espere um mapa que tenha a chave name e guarde o valor dela na variável name_var.

Agora imagine que além de querer extrair essa variável, queremos também ter certeza que o ano é 2022.

Poderíamos implementar a função da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
defmodule UserUtils do
  def extract_name(%{name: name_var, year: 2022}) do
    IO.puts "Name: #{name_var}"
  end
end

# chamada da função:
user = %{name: "Rodrigo", year: 2022}
UserUtils.extract_name(user)

# resultado:
# Name: Rodrigo

agora passando um mapa que não casa com o padrão:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
defmodule UserUtils do
  def extract_name(%{name: name_var, year: 2022}) do
    IO.puts "Name: #{name_var}"
  end
end

# chamada da função:
user = %{name: "Rodrigo", year: 2021} # 2021 aqui não casa com o esperado pela função
UserUtils.extract_name(user)

# resultado:
** (FunctionClauseError) no function clause matching in UserUtils.extract_name/1

The following arguments were given to UserUtils.extract_name/1:

    # 1
    %{name: "Rodrigo", year: 2021}

    #cell:2: UserUtils.extract_name/1

Dito isso podemos ter um fallback pra qualquer outro valor que não seja o esperado no match, já que caso não exista essa implementação vamos sempre tomar o erro como no exemplo acima. No exemplo a seguir utilizamos outra funcionalidade da linguagem que é o multi clause function que nos permite junto com pattern matching redeclarar uma função com argumentos diferentes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
defmodule UserUtils do
  def extract_name(%{name: name_var, year: qualquer_outro_ano}) do
    IO.puts "Name: #{name_var}, ano: #{qualquer_outro_ano}"
  end

  def extract_name(%{name: name_var, year: 2022}) do
    IO.puts "Name: #{name_var}"
  end

  
end

# chamada da função:
user = %{name: "Rodrigo", year: 2021}
UserUtils.extract_name(user)

# resultado
Name: Rodrigo, ano: 2021
:ok

A ordem das funções aqui importa, então o primeiro match que tem um padrão específico irá ser executado. Caso queira testar mude a ordem das funções e verá que mesmo que exista um match específico sempre a primeira instrução será executado

Caso não tenha reparado de início, o multi clause function nos permite por exemplo remover um if desnecessário (um para ano 2022 e qualquer outra coisa para ano != 2022), nós literalmente definimos funções para cada situação. No mundo real é mto comum encontrar funções com essas características, ex:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
  def find(html_tree, selector_as_string) when is_binary(selector_as_string) do
    selectors = get_selectors(selector_as_string)
    find_selectors(html_tree, selectors)
  end

  def find(html_tree, selectors) when is_list(selectors) do
    find_selectors(html_tree, selectors)
  end

  def find(html_tree, selector = %Selector{}) do
    find_selectors(html_tree, [selector])
  end

O exemplo acima vem da lib floki. Perceba que temos a função find declara para vários cenários:

  • Quando o segundo argumento é uma string
  • Quando o segundo argumento é uma lista
  • Ou quando o segundo argumento é um tipo específico

Para complementar a parte de funções, veja esse exemplo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# https://womanonrails.com/elixir-pattern-matching
defmodule Math do
  def minus?(), do: "No number"
  def minus?(x), do: x < 0
  def minus?(x, 2), do: "Suprice #{x}!"
  def minus?(x, y), do: x < 0 && y < 0
end

Math.minus?
#=> "No number"
Math.minus?(1)
#=> false
Math.minus?(1, 2)
#=> "Suprice 1!"
Math.minus?(1, 3)
#=> false
Math.minus?(1, -3)
#=> false
Math.minus?(-1, -3)
#=> true

head and tail

Uma operação comum em listas e que aparece sempre em recursão é a utilização do head and tail, que basicamente é extrair o primeiro valor de uma lista (head) e ter o resto dela (tail), ex:

1
2
3
4
5
6
iex> [head | tail] = [1, 2, 3]
[1, 2, 3]
iex> head
1
iex> tail
[2, 3]

O mesmo resultado pode ser obtido da seguinte forma:

1
2
3
4
5
iex> list = [1, 2, 3]
iex> hd(list)
1
iex> tl(list)
[2, 3]

Pin operator

Variáveis no elixir podem ser reatribuídas/atualizadas e caso você não queira que isso aconteça (comum em comparações e fluxos específicos baseados em decisões (case/cond/etc)), o pin operator (^) pode ser utilizado:

1
2
3
4
5
# https://elixir-lang.org/getting-started/pattern-matching.html
iex> x = 1
1
iex> ^x = 2
** (MatchError) no match of right hand side value: 2

na segunda expressão nós evitamos que a variável seja reatribuida, nós estamos expressando: “Essa expressão só é valida se o valor for igual ao atribuido anteriormente”

Em outras operações:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# https://elixir-lang.org/getting-started/pattern-matching.html
iex> x = 1
1
iex> [^x, 2, 3] = [1, 2, 3]
[1, 2, 3]
iex> {y, ^x} = {2, 1}
{2, 1}
iex> y
2
iex> {y, ^x} = {2, 2}
** (MatchError) no match of right hand side value: {2, 2}

Na última expressão o erro aparece porque já definimos o x = 1 e queremos apenas uma comparação e não uma atribuição

Outras operações

O pattern matching aparece em muitas operações, e sabendo como funciona é possível executar fluxos complexos ou simplificados de várias formas:

case

1
2
3
4
5
case {:ok, "Hello World"} do
  {:ok, result} -> result
  {:error} -> "Uh oh!"
  _ -> "Catch all"
end
1
2
3
4
5
6
case {1, 2, 3} do
  {1, x, 3} when x > 0 ->
    "Will match"
  _ ->
    "Won't match"
end

maps

1
2
3
4
5
6
7
8
> key = "hello"
"hello"

> %{^key => value} = %{"hello" => "world"}
%{"hello" => "world"}

> value
"world"

funções / funções anônimas

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
> greeting = "Hello"
"Hello"

> greet = fn
  (^greeting, name) -> "Hi #{name}"
  (greeting, name) -> "#{greeting}, #{name}"
end

> greet.("Hello", "Sean")
"Hi Sean"

> greet.("Mornin'", "Sean")
"Mornin', Sean"

> greeting
"Hello"

tuplas

1
2
3
4
5
> {:ok, value} = {:ok, "Successful!"}
{:ok, "Successful!"}

> value
"Successful!"

Conclusão

Quanto mais a vontade se sentir com pattern matching, mais seu código ficará legível / pragmático :)