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! :)

Usando pattern matching e recursividade com Elixir

Uma das linguagens de programação que mais tem me animado ultimamente é a Elixir. Ela possui muitas features legais, e uma delas é o pattern matching.

Patter maching é uma feature, assim como muitas outras, herdada da Erlang, que é a linguagem em que Elixir se basea. Em uma tradução livre, pattern maching quer dizer “casamento de padrão”. O termo é meio exquisito em português, mas vamos a um exemplo:

1
{:ok, name} = {:ok, "Philip"}

Nesse exemplo, estamos “casando” o que está do lado direito com um padrão do lado esquerdo. Explico: do lado esquerdo temos uma tupla com o primeiro elemento :ok e o segundo elemento como uma variável name.

A linguagem vai tentar fazer bater o tipo e o conteúdo dos dois lados. No caso, o tipo bate: – é uma tupla; – o primeiro elemento da tupla bate: :ok é :ok; – o segundo argumento também bate: do lado esquerdo temos uma variável a ser “preenchida”, do lado direito temos um valor para ela.

Agora um exemplo onde o pattern não bate:

1
2
{:ok, "João" } = {:ok, "Philip"}
# => ** (MatchError) no match of right hand side value: {:ok, "Philip"}

Por mais que o tipo do segundo elemento seja o mesmo, não há casamento porque os conteúdos das strings são diferentes.

Usando pattern matching em funções

Um dos usos mais poderosos para o pattern maching é na declaração de funções.

Em Elixir, as funções são definidas por três combinações: o seu nome, o número de argumentos, e o “padrão” desses argumentos. Em suma, são definidas pelo nome e argumentos.

Isso quer dizer que uma função somar(numero1, numero2) é diferente da função somar(numero1, numero2, numero3). E as funções check({200, body}) e check({404, body}) são diferentes por conta do padrão de seus argumentos (a diferença do primeiro elemento da tupla).

Uma aplicação muito legal pra isso é na construção de funções recursivas.

O exemplo a seguir mostra como somar números de uma lista através de recursividade. Ele é bem parecido com o encontrado no guia Getting started do site oficial da linguagem, mas considero o meu (IMHO) um pouco mais simples.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defmodule Math do
  def sum_list([]) do
    0
  end

  def sum_list([head|tail]) do
    head + sum_list(tail)
  end
end

# Exemplos:
#
#   Math.sum_list([10])
#   # => 10
#
#   Math.sum_list([17, 13, 7, 5])
#   # => 42
#
#   Math.sum_list([])
#   # => 0

Como você pode perceber, os nomes das funções são idênticos, mas o que muda são seus argumentos. Todas as três recebem listas, porém cada uma com um padrão diferente:

  • A primeira definição recebe uma lista vazia, e como uma lista vazia não tem números, retorna zero;
  • A terceira função recebe uma lista e divide ela em duas partes: cabeça e “calda”.
    • A cabeça (head) é composta por um único elemento, que é o primeiro elemento da lista;
    • A “calda” é o restante da lista. Então uma lista [2, 3, 4] será dividida em head = 2 e tail = [3, 4]. Feito isso, a função ira somar recursivamente o primeiro elemento da lista com o resultado da próxima chamada da função recebendo a lista da calda.

Quando há somente um item na lista, head é igual a esse elemento, e tail é vazia ([]).

As chamadas recursivas de “sum_list” serão processadas levando em conta o “padrão” dos argumentos passados. A “condição de parada” da terceira função é quando tail for uma lista com somente um elemento. Neste caso a função que será utilizada é a segunda, que recebe uma lista com um elemento.

Para onde ir agora?

Nos exemplos usados aqui vimos um pouco do que é possível fazer com o uso de pattern matching. Essa não é uma característica exclusiva da Elixir, mas para mim ficou muito mais fácil de entender com essa linguagem (já havia tentado com Haskell).

Experimente implementar os exemplos e deixe suas dúvidas e sugestões nos comentários.

Recomendo esses links para mais detalhes da linguagem:

Executando seus testes em uma App Rails na velocidade máxima com Vim e Zeus

Configurei meu Vim para rodar os testes da minha aplicação Rails (dependendo ou não do Rails) bem mais rápidos, através do Zeus.

O Zeus pré-carrega (faz um preload) sua aplicação Rails, e tudo que é dependência dela. Você pode instalá-lo com o comando:

1
$ gem install zeus

É importante lembrar que o Zeus não deve ser declarado no seu Gemfile.

Para executar o Zeus, vá a seu projeto Rails e execute zeus start. A partir daí, você conseguirá subir seu servidor (zeus s) ou executar comandos (zeus rake/generate/etc) de forma muito rápida!

Para rodar seus testes usando RSpec direto do seu Vim, você pode usar o plugin vim-rspec. Instale-o pela maneira mais recomendada, usando Vundle ou a que você preferir.

Em seguida é necessário configurar o comando que executa os testes. Adicione a linha abaixo ao seu .vimrc:

1
let g:rspec_command = "! zeus rspec {spec}"

O comando padrão para executar os testes (todos do arquivo) é \\t (<Leader>t) Se quiser executar um teste específico, mova o cursos para ele e execute \\s(<Leader>s). Há também um comando para executar o último teste: \\l(<Leader>l)).

Com o rspec-vim sua produtividade pode aumentar consideravelmente, evitando a mudança de abas ou terminais para executar seus códigos.

Declaração de variável no Ruby: por valor ou referência?

Toda declaração de variável na linguagem Ruby é feita por referência. Isso significa que quando declaramos uma variável, na verdade estamos declarando uma referência para um objeto em memória. Essa informação parece ser básica e dificilmente você terá problemas com isso. Mas aconteceu comigo, onde por engano eu esqueci deste pequeno detalhe.

Meu caso foi com um código mais ou menos como o abaixo.

1
2
3
4
5
6
7
8
9
a = [1, 2, 3, 4]
b = a
b << 5
puts b
#=> [1, 2, 3, 4, 5]
puts a
#=> [1, 2, 3, 4, 5]
puts a.object_id == b.object_id
#=> true

O código não era tão simples, mas o exemplo ilustra bem como aconteceu. Meu intuito era trabalhar com o conteúdo “armazenado” em b e deixar o conteúdo em a intacto. Porém quando alterei o valor de b (adicionando 5), também alterei em a. Isso causou um bug que foi corrigido no processo de code review.

Isso porque em Ruby todas as declarações são feitas através de referências para objetos.

Para resolver possíveis problemas (como alterar acidentalmente o objeto errado), você pode “clonar” ou “duplicar” (veja a diferença nos links abaixo) um objeto quando for referenciá-lo a uma variável.

Exemplo:

1
2
3
4
5
6
7
8
9
a = [1, 2, 3, 4]
b = a.dup
b << 5
puts b
#=> [1, 2, 3, 4, 5]
puts a
#=> [1, 2, 3, 4]
puts a.object_id == b.object_id
#=> false

Alguns links do Stack Overflow sobre o assunto: