Blog do Phil

Tecnologia e opinião

10 exemplos de pattern matching em Elixir

Pattern in airplanes

Como no texto anterior, hoje vou falar sobre o uso de pattern matching em Elixir de uma forma mais detalhada.

Pattern matching é um recurso muito poderoso (quase mágico!). Vou começar demonstrando simples exemplos, até chegar aos mais complexos.

1) Associando valores

Em Elixir não existe o conceito de variáveis como vemos nas linguagens de programação “convencionais”. Isso occore porque em Elixir não temos estado (na verdade temos, mas é um assunto pra outro post): temos valores que são transformados de função em função.

Quando se cria uma expressão de associação (ex.: x = 10), na verdade o que ocorre é uma tentativa de “apelidar” um valor a um nome (10 foi apelidado de x). Essa tentativa é feita através de pattern matching.

Exemplo:

1
2
3
4
5
6
7
8
9
10
# Até aqui, tudo normal
number = 42

# Um teste estranho, mas dá certo porque o valor da direita é igual o da esquerda
42 = number

# Uma tentativa de match que vai gerar um erro
52 = number

# ** (MatchError) no match of right hand side value: 42

A linguagem vai tentar sempre “casar” o valor da esquerda com o da direita.

2) Um pouco mais complicado: Associando valores dentro de listas

Agora queremos capturar valores dentro de uma lista simples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Um exemplo que funciona
[a, b, c] = [5, 7, 13]

IO.puts(a)
# => 5

IO.puts(b)
# => 7

IO.puts(c)
# => 13

# Um exemplo com um valor igual do lado esquerdo:
[d, 19, f] = [17, 19, 23]

IO.puts(d)
# => 17
IO.puts(f)
# => 23

Como você pode ver, Elixir vai entender que o valor do elemento do meio é igual nos dois lados. Mas caso esse valor seja diferente, não há match.

Exemplo com um erro de match:

1
2
3
# Com um valor diferente do lado esquerdo:
[d, 63, f] = [17, 19, 23]
# ** (MatchError) no match of right hand side value: [17, 19, 23]

Os mesmos exemplos podem ser reproduzidos usando tuplas:

1
2
3
4
5
6
7
8
9
10
11
{a, b, c} = {1, 2, 3}

{:ok, status_code} = {:ok, 200}

IO.puts(status_code)
# => 200

{d, 2, e} = {1, 2, 3}

{:ok, status_code} = {:error, 500}
# ** (MatchError) no match of right hand side value: {:error, 500}

É possível até repetir o nome da “variável”:

1
2
3
4
5
6
7
{a, b, a} = {1, 2, 1}
IO.puts(a)
# => 1

# Quando se repetem mas os valores são diferentes, ocorre um erro de `match`:
{d, c, d} = {3, 4, 5}
# ** (MatchError) no match of right hand side value: {3, 4, 5}

3) Para pegar o primeiro item de uma lista e o restante da lista

O conceito de head e tail é muito usado em linguagens funcionais, e não é exceção em Elixir. Com ele é possível pegar o primeiro item da lista e o restante da lista em uma só expressão de associação.

Li pela primeira vez sobre o assunto em um tutorial de haskell, que recomendo!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
list = [1, 2, 3, 4, 5, 6, 7]

# Sobre a lista `list`, podemos dizer:
#
# O primeiro item, que vamos chamar de `head`,
# possue valor 1.
#
# Todo o resto da lista, que vamos chamar
# de `tail`, possue o valor = [2, 3, 4, 5, 6, 7]
#
# Para capturar esses valores, faremos o seguinte:

[head|tail] = list

IO.puts(head)
# => 1

IO.puts(tail)
# => [2, 3, 4, 5, 6, 7]

4) Para pegar o segundo, ou n-itens do começo de uma lista

Seguindo o exemplo anterior, para dar match nos dois primeiros itens de uma lista:

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

[h1, h2|t] = list

IO.puts(h1)
# => 1

IO.puts(h2)
# => 2

IO.puts(list)
# => [3, 4]

5) Para filtrar alguns resultados de uma lista usando list comprehensions

Aqui começa a complicar. Quem já programou em Python deve ter usado um recurso bem interessante: list comprehensions.

Em Elixir também temos comprehensions que podemos usar em listas, tuplas, binaries e streams.

Primeiro, um exemplo usando associação simples:

1
2
for n <- [1, 2, 3], do: n * n
# => [1, 4, 6]

Agora, um exemplo usando um padrão mais complicado:

1
2
3
4
5
6
7
8
9
10
statuses = [
  {:error, 404},
  {:ok, 201},
  {:error, 500},
  {:ok, 301}
]

for {:error, n} <- statuses, do: IO.puts("Error #{n}")
# => Error 404
# => Error 500

Como o exemplo sugere, comprehensions retornam somente os itens que deram match no padrão. Isso possibilita o uso de um filtro sem ter que escrever muito código.

6) Para dividir uma string em partes

É comum percorrer uma string (ou binary) por cada caracter. Podemos fazer isso usando um match de binary, como no exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
name = "Philip"

<< h::utf8, t::binary >> = name

IO.puts(t)
# => "hilip"

IO.puts(h)
# => 80 # a versão binária de "P"

IO.puts(<< h::utf8>>)
# => "P"

É interessante notar que estou especificando o encoding do binary. Isso é necessário para que a linguagem entenda aquele caracter como sendo parte de uma string.

Algo a se notar também é que usei uma vírgula ao invés de um pipe (|) para separar head e tail da string. Para que a representação de h seja a correta, tenho que dizer que aquele binary é UTF8.

7) Outra forma para dar match em uma string

Há uma maneira mais simples de dar match em uma string usando um operador de concatenação de strings/binaries: o <>.

Sem matches, o usamos da seguinte forma:

1
2
"Phil" <> "ip"
#=> "Philip"

Agora com o match:

1
2
3
4
5
"Philip" = "Phil" <> sufix
#=> "Phil"

IO.puts(sufix)
#=> "ip"

É bem simples e útil quando se sabe o conteúdo do início de uma string, e queremos pegar o restante dela que é variável, como estou usando nesse parser e leitor de HTML.

8) Usando o case

Pattern matching é usado com frequência em um bloco case que serve justamente para fazer alguma ação de acordo com o padrão de entrada. É como o case de C, no sentido de ter vários branches de decisão, mas ao invés de tomar decisões por condições booleanas, o case da Elixir toma decisões baseado em padrões.

Exemplo:

1
2
3
4
5
6
7
# Para abrir um arquivo, usamos `File.open/1`
case File.open("name-of-file.txt") do
  {:ok, contents} ->
    IO.puts("You use this contents: #{contents}")
  {:error, reason} ->
    IO.puts("ERROR while open file: #{reason}")
end

O case é a provavelmente a estrutura de controle mais usada em Elixir. Faz parte da filosofia da linguagem escrever os programas levando em conta situações adversas, como o erro que está sendo considerado no exemplo.

Ao invés de usar um tratamento de exceção, a linguagem propõe que os erros sejam tratados como caminhos “normais” do programa. Sendo assim, o case é bem prático para casar um padrão em situações onde um erro é esperado.

Podemos também usar o casamento de padrão em funções, como demonstrei no post anterior.

9) Para dar match em qualquer coisa

É possível ignorar um determinado valor dentro de um match usando o underscore. Ele serve como um coringa, e é muito útil em determinadas ocasiões.

Exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
values = {1, 2, 3}

case values do
  {1, 5, 3} -> IO.puts("5 in the middle")
  {1, _, 3} -> IO.puts("whatever in the middle")
end
# => "whatever in the middle"

# Outro exemplo de case,
# dessa vez com _ para todo o valor:

case values do
  {1, 5, 3} -> IO.puts("5 in the middle")
  _ -> IO.puts("default case when nothing else matches")
end
# => default case when nothing else matches

10) Usando em blocos de comunicação entre processos

Uma das features mais legais de Elixir é o modelo de comunicação entre atores (ou processos). Com ela é possível enviar todo o tipo de mensagem para um processo e processar essa mensagem através de pattern matching. Esse assunto é bem complicado de início, por isso recomendo que leiam um pouco mais sobre isso.

Exemplos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Para criar um novo processo que irá
# receber mensagens, faça:

pid = spawn(fn ->
              receive do
                {:ok, msg} ->
                  IO.puts("Received: #{msg}")
                _ ->
                  IO.puts("I don't understand you! :/")
              end
            end)

# Envie uma mensagem para o processo:

send(pid, {:ok, "Elixir is awesome!"})
# => Received: Elixir is awesome

Perceba que o bloco de receive é bem parecido com o case, porém está trabalhando com uma mensagem recebida.

Conclusão

Nesse artigo mostrei alguns exemplos de pattern matching em Elixir. Para entender mais e melhor, recomendo a leitura do livro Programming Elixir do Dave Thomas que é bem completo. Recomendo também dar uma fuçada em projetos abertos e tentar entender o código.

Deixo aqui meu convite à comunidade brasileira de Elixir, que é bem pequena ainda, a escrever mais sobre a linguagem. Em português ou inglês, o importante é trazer o máximo de devs que puder! :)

Comments