Casamento de padrões em Elixir

Publicado em 16 out 2020. Uns 11 minutos de leitura.

Um desafio de programação é conseguir escrever código de maneira mais sucinta sem sacrificar a facilidade de compreensão. É comum ver soluções de uma linha que resolvem o problema e ninguém entende como elas funcionam. Pensando nisso, algumas linguagens trazem sintaxe que permite escrever, de maneira idiomática, código mais expressivo. Um exemplo disso é casamento de padrões. Recentemente, comecei a estudar Elixir, e fiquei feliz em descobrir que pattern matching é uma parte importante da linguagem e resolvi trazer um pouco disso pra cá!

Casamento de padrões é uma maneira de verificar se uma estrutura de dados tem um determinado formato. Nessa verificação, é possível extrair informação dessa estrutura. Um exemplo da intuição de casamento de padrões é o destructuring, bastante comum em JavaScript moderno:

const person = { name: 'Jane Doe', age: 31 }
const { name } = person
console.log(name) // Imprime 'Jane Doe'

Neste exemplo, na segunda linha, nós afirmamos que a estrutura de dados person tem o formato de um objeto com uma chave name. Como esse nome de variável está livre, atribuimos a ele o valor do atributo name do objeto. Destructuring porém apenas permite casar um objeto com uma lista de atributos, ou um array com uma lista de posições. Ele não nos permite fazer afirmações mais específicas sobre o formato da estrutura de dados ou despachar de maneira condicional a este formato.

Casando padrões em Elixir

Elixir é uma linguagem em que casamento de padrões faz parte de suas práticas idiomáticas. Nela, casamento de padrões nos permite fazer destructuring como no exemplo em JavaScript, porém também nos permite ir bem além. Vamos começar com a sintaxe de casamento de padrões e depois ver algumas aplicações.

Em Elixir, o operador = é chamado de operador de match (casamento), e não de atribuição como em outras linguagens. Ele executa um casamento de padrões entre o lado esquerdo e direito do operador.

x = {:alice, :bob}
# Note que, em Elixir, `{}` delimitam uma tupla e não um objeto ou mapa, como em JavaScript. 

No exemplo acima, estamos fazendo um match de x com a tupla {:alice, :bob}. x é um identificador e a tupla é uma estrutura de dados. É possível que x tenha o mesmo formato que a tupla? Sim, se x tiver como valor {:alice, :bob}. E é isso que vai acontecer a partir dessa linha. Quando colocamos apenas um identificador do lado esquerdo do match, o operador funciona exatamente como uma atribuição.

{:alice, other_name} = {:alice, :bob}
IO.puts(other_name) # Imprime :bob

Neste exemplo, fazemos um destructuring usando casamento de padrões. Nós definimos, do lado direito, que temos uma tupla cujo formato é ter :alice no primeiro elemento e um identificador other_name no segundo elemento. Em seguida, casamos isso com a tupla {:alice, :bob}. A única maneira de esse casamento dar certo é se other_name tiver o valor :bob, e é isso que vai acontecer nessa linha.

Note que só podemos usar identificadores para atribuir variáveis em casamento de padrões do lado esquerdo do operador de match. Do contrário recebemos um erro de compilação.

{:alice, :bob} = {:alice, other_name}
# ** (CompileError) iex:1: undefined function other_name/0

Similarmente, recebemos um erro se os padrões não casarem:

{:alice, other_name} = {:charlie, :bob}
# ** (MatchError) no match of right hand side value: {:charlie, :bob}

Podemos casar outras estruturas de dados, também. Mapas, delimitados com %{}, podem casar com um subconjunto das suas chaves e listas podem casar com outra lista de mesmo tamanho ou com uma representação no formato [cabeça_da_lista | resto_da_lista]:

%{first_name: name} = %{first_name: "Jane", last_name: "Doe"}
IO.puts(name) # Imprime "Jane"

[first_item | other_items] = [1, 2, 3, 4, 5]
IO.puts(first_item) # Imprime 1
IO.puts(other_items) # Imprime [2, 3, 4, 5]

Como comentei no começo, identificadores no lado esquerdo do operador são tratados como uma atribuição de variável, usando o valor que casa do lado direito. É possível evitar essa atribuição e usar uma variável do lado esquerdo do operador de pin (fixação), ^. Com ele, podemos fixar o valor da variável do lado esquerdo e usá-lo como parte do padrão a casar, e não como um destino para uma atribuição.

result = :ok
{^result, message} = {:ok, "Your request was processed"}
IO.puts(message) # Imprime "Your request was processed" e não muda o valor de result

{^result, message} = {:error, "Your request was not processed"}
# ** (MatchError) no match of right hand side value: {:error, "Your request was not processed"}

Despachando com casamento de padrões

Até agora vimos como casar padrões em Elixir. Porém, com o que vimos nós conseguimos apenas fazer atribuições de variáveis e destrucutring, além de gerar um erro quando o padrão não casa. Tirando o erro, não temos nada muito novo. Porém, podemos aplicar casamento de padrões a condicionais e funções para tomar caminhos diferentes no programa de acordo com o casamento ou não de padrões.

# Imagine uma consulta ao banco de dados que retorna quantos elementos existem.
# O retorno é uma tupla com um código de status e um valor.
result = {:ok, 50}

message = case result do
  {:ok, element_count} -> "There are #{element_count} elements"
  {:error, error_message} -> "An error occurred: #{error_message}"
end

IO.puts(message) # Imprime "There are 50 elements"

Podemos usar a estrutura de controle case para casar padrões. Ela recebe uma entrada, result no nosso exemplo, e uma lista de possíveis padrões para tentar casar. Caso haja algum que dê match, o valor para que ele aponta (->) é retornado pelo case. Note que a lista de padrões a casar é equivalente ao lado esquerdo do operador de match. Ou seja, podemos colocar identificadores que se tornam variáveis no corpo após o -> e podem ser usados no valor retornado.

É possível que o valor de entrada case com mais de um padrão, dependendo de como a lista seja especificada. Por isso, a ordem importa. Quando o primeiro padrão casar, os seguintes serão ignorados.

Vamos re-escrever o exemplo acima numa função para testar os diferentes retornos possíveis:

defmodule MockDB do
  def display_result(result) do
    case result do
      {:ok, element_count} -> "There are #{element_count} elements"
      {:error, error_message} -> "An error occurred: #{error_message}"
    end
  end
end

MockDB.display_result({:ok, 42})
# Imprime "There are 42 elements"

MockDB.display_result({:error, "DB is offline"})
# Imprime "An error occurred: DB is offline"

MockDB.display_result({:palha_italiana, "Really tasty"})
# ** (CaseClauseError) no case clause matching: {:palha_italiana, "Really tasty"}
#     iex:10: MockDB.display_result/1

Caso não haja um padrão que case, tomamos um erro. É possível tratar esse caso garantindo que, ao fim da lista, sempre haverá um padrão que case. Podemos colocar um identificador qualquer (x -> "????"), mas isso vai criar uma variável local de que não precisamos. Ao invés de usar x, podemos usar o identificador especial _. Não é possível ler dessa variável, e podemos atribuir a ela sem nos importar.

Além disso, podemos ter casamentos com partes similares, desde que a mais específica fique acima da mais geral. Por exemplo, podemos querer tratar separadamente o caso em que haja exatamente um elemento retornado.

defmodule MockDB do
  def display_result(result) do
    case result do
      {:ok, 1} -> "There is only one element"
      {:ok, element_count} -> "There are #{element_count} elements"
      {:error, error_message} -> "An error occurred: #{error_message}"
      _ -> "????"
    end
  end
end

MockDB.display_result({:ok, 1})
# Imprime "There is only one element"

MockDB.display_result({:palha_italiana, "Really tasty"})
# Imprime "????"

O case não é a única maneira de despachar de acordo com casamento de padrões! Como comentei, Elixir define casamento de padrões como parte idiomática da linguagem, e seu uso vai além disso. Podemos usar casamento de padrões para mudar a execução de uma função. Vamos re-escrever o exemplo acima mais uma vez, com essa ideia:

defmodule MockDB do
  def display_result({:ok, 1}) do
    "There is only one element"
  end

  def display_result({:ok, element_count}) do
    "There are #{element_count} elements"
  end

  def display_result({:error, error_message}) do
    "An error occurred: #{error_message}"
  end

  def display_result(_) do
    "????"
  end
end

MockDB.display_result({:ok, 1})
# Imprime "There is only one element"

MockDB.display_result({:ok, 42})
# Imprime "There are 42 elements"

MockDB.display_result({:error, "DB is offline"})
# Imprime "An error occurred: DB is offline"

MockDB.display_result({:palha_italiana, "Really tasty"})
# Imprime "????"

Podemos usar casamento de padrões nos argumentos de uma função. Declaramos mais de uma vez uma mesma função, porém em cada declaração, definimos um padrão diferente para o primeiro argumento. Cada vez que essa função for chamada, Elixir vai, em ordem, tentar casar os argumentos com os padrões e, quando encontrar um casamento que dê match, o corpo dessa declaração é executado.

Nossas funções neste caso apenas retornam strings, mas nada impede que cada uma faça algo totalmente diferente. Isso nos permite, rapidamente, saber que formatos de dados essa função está pronta para lidar. Como definimos um padrão que sempre vai casar com qualquer coisa (o _), essa função nunca vai retornar um erro de casamento de padrões.

Vamos ver outro exemplo, dessa vez fazendo casamento um valor mais simples. Ao invés de casar uma tupla, vamos casar um número. Podemos fazer isso para implementar um cálculo de fatorial. O fatorial de um número n é o produto de todos os números inteiros de 1 até n. Fora isso, por definição, o fatorial de zero é 1.

# Definição de como funciona o fatorial
factorial(0) == 1
factorial(n) == n * factorial(n - 1)

Podemos implementar esse fatorial, de maneira recursiva, usando casamento de padrões.

defmodule Math do
  def factorial(number) do
    factorial(number, 1)
  end

  defp factorial(0, current_product) do
    current_product
  end

  defp factorial(number, current_product) do
    factorial(number - 1, number * current_product)
  end
end

Math.factorial(0) # Retorna 1
Math.factorial(1) # Retorna 1
Math.factorial(20) # Retorna 2432902008176640000

Temos duas funções: uma que recebe um argumento (também chamada de Math.factorial/1) e outra que recebe dois argumentos (chamada Math.factorial/2). A primeira serve apenas de conveniência, e é pública. Ela chama a segunda inicializando o produto com 1. Na função com dois argumentos é que temos um casamento de padrões. Vamos tentar, primeiramente, casar o primeiro argumento com o padrão 0. A única maneira desse casamento bater é se o argumento for, de fato, 0.

Se o padrão casa, nós retornamos o produto acumulado até agora (segundo argumento da função). Se ele não casa, nós tentamos a segunda definição da função. Na segunda definição, o casamento vai com certeza acontecer pois temos apenas identificadores em todos os argumentos. O que essa definição faz é chamar, recursivamente, a função subtraindo 1 do primeiro argumento e multiplicando o produto acumulado pelo primeiro argumento.

Lembre-se que a ordem é importante no casamento de padrões. Se invertêssemos e colocássemos o caso em que o primeiro argumento é 0 embaixo, ele nunca seria executado pois o valor 0 iria casar com o outro caso primeiro, o que é apenas uma variável number.

Um benefício adicional dessa implementação é que ela é uma recursão de cauda (tail recursion), ou seja, cada iteração da recursão não precisa de espaço adicional na pilha de chamadas de função. Isso é útil pois, do contrário, ao tentar calcular um fatorial muito grande podemos ter um erro de estouro de pilha. Note também que, caso você passe um valor negativo para a função, ela vai executar infinitamente. Uma maneira de evitar isso é usando guards.

Pra onde vou agora?

Trouxe aqui um pouco de como funciona casamento de padrões em Elixir e alguns exemplos de como utilizar para escrever condições mais claras. Com isso, conseguimos por exemplo separar fluxos de execução de acordo com o formato de uma estrutura de dados.

A documentação oficial de Elixir é bem detalhada e traz ainda mais conteúdo sobre o assunto. Eu comecei a usar a linguagem para aprender, e os tutoriais da documentação ajudaram bastante. Se você quer começar, sugiro partir do guia de introdução e seguir o passo a passo. Sobre esses assuntos que vimos, essas páginas são bem úteis:

E se esse post te trouxe ideias, ou se tem sugestões de como melhorar algum dos exemplos, me dá um oi no Twitter! 👋