Pattern Matching

Pattern matching em variáveis

Toda linguagem de programação tem a capacidade de criar variáveis. Você define um nome (talvez um tipo, dependendo da linguagem) e assina um valor a ela.

Elixir é um pouco diferente, mesmo parecendo igual a primeira vista, seu conceito muda.

Vamos a um exemplo simples onde criaremos uma map chamado pessoa e dentro dele teremos o campo name e genre. Popularemos os valores do map com My Name e :no_binary.

Vamos ao teste.

test/person_test.exs
defmodule SimpleTest do
  use ExUnit.Case

  test "simple tests about pattern matching" do
    person = Person.create("My Name", :no_binary)

    assert person.name == "My Name"
    assert person.genre == :no_binary
  end
end

Rodando esse teste, obtemos o relatório de erro:

$ mix test
warning: Person.create/2 is undefined (module Person is not available or is yet to be defined)
  test/phrases_test.exs:6: SimpleTest."test simple tests about pattern matching"/1

.

  1) test simple tests about pattern matching (SimpleTest)
     test/phrases_test.exs:4
     ** (UndefinedFunctionError) function Person.create/2 is undefined (module Person is not available)
     code: person = Person.create("My Name", :no_binary)defmodule Document do
  def render(_id, _type) do
    "Rendering txt"
  end
end

     stacktrace:
       Person.create("My Name", :no_binary)
       test/phrases_test.exs:6: (test)

.
Finished in 0.04 seconds (0.00s async, 0.04s sync)
3 tests, 1 failure

Vamos criar nosso módulo para passar o teste

Essa função apenas retorna um map com os valores que enviamos.

Se rodarmos novamente, os testes terão passados.

Analisando nosso teste, a definição de person parece uma assinatura comum de variável, certo? chave = valor. Mas por traz dos panos, elixir utiliza pattern matching para fazer isso. O valor a direita, apenas será adicionado a esquerda, caso de o match.

Em nosso exemplo, o requisito para matching é: se possuir algo ao lado direito, ele conseguira ser atribuído a variável person. Ele é bem aberto e pode receber qualquer tipo de dado.

Sei que parece confuso. Mas vamos continuar, logo tudo fará sentido.

Imagine agora que voce precisa pegar apenas name e genre que estão dentro do map retornado da função e coloca-los em uma variável cada. Para poder fazer as confiramações isoladas.

Algo assim:

Sem pattern matching, voce precisaria continuar com o valor person.name ou atribuir ele a uma nova variável new_name = person.name. O que por fim, daria na utilização do mesmo.

Com pattern matching é diferente, você pode obter diretamente o valor que quer, sem precisar de uma etapa intermediária.

O new_name e new_genre estão na posição onde está onde deveriam estar os valores certo? Ele é um espelho do que tem dentro da função, porem, no lugar dos valores, temos a definição de uma nova variável e é ai que o matching acontece. No nosso caso, estamos so dizendo que aceitamos qualquer valor que siga a estrutura de dentro do map, tendo name sendo chamado agora de new_name e a mesma coisa acontece com new_genre.

Sendo assim, podemos utilizar diretamente a variável, porque após o matching, ela existe.

Se rodar o teste a cima, ele funcionará corretamente.

Outros exemplo:

circle-info

IO.inspect

Função do módulo IO para espionar o que tem dentro de algum elemento. Mais sobre IO.inspectarrow-up-right

Quando um matching não é sucedido ele dispara uma exceção.

Pattern matching em funções

Na função funciona do mesmo jeito, porém, utilizado nos parâmetros. Vamos a um exemplo. Precisamos de uma função que exiba um tipo de documento dependendo do tipo pedido. Vamos chamar essa função de render e o módulo de Document. Nossa função precisará de um ID para identificar o documento e um atom pedindo o tipo apropriado, nesse exemplo :txt, :pdf. Vamos cuidar do txt primeiro:

Rodando esse teste obteremos um relatório comum de erro.

Nosso módulo ainda não existe. Vamos cria-lo:

Rodando o teste agora, tudo vai estar funcionando.

Vamos lidar agora com o PDF.

Se rodarmos novamente, teremos um relatório de erro.

Aqui as coisas começam a ficar interessantes. Estamos chamando a mesma função, mas com parâmetros diferente. Uma solução fácil para isso, é por um if dentro da função e retornar o valor que queremos. Algo assim:

Isso funcionária, mas se tornaria uma bagunça se precisarmos colocar mais tipos de arquivos. Com pattern matching, podemos controlar o fluxo das chamadas de funções, colocando nos parâmetros a formula do padrão para executar uma função.

Na definição da função, no segundo parâmetro, ao invés de colocar uma variável, setamos diretamente o valor. Por regra do Pattern matching, o valor deve ser igual para ele ser realizado com sucesso e ai executar a função. A primeira função render so irá ser executada, quando o segundo parâmetro for :txt e a segunda função apenas quando for :pdf.

Se rodar o código, teremos um relatório positivo.

Caso nenhuma dessas condições seja cumprida, uma exceção é lançada. Para evitar isso, podemos criar uma função para avisar que o tipo não é suportado:

executando o teste, temos o seguinte relatório:

O próprio relatório nos da as opções válidas com :txt e :pdf. Vamos corrigir isso.

Na ultima função, troquei novamente o segundo parâmetro para variável, assim ele não espera um valor especifico e consegue executar a função. Tendo isso em mãos, criamos uma frase para avisar que o tipo informado não é válido.

Podemos rodar esse código e temos o relatório de sucesso:

circle-info

Ordem de execução

A tentativa de execução de funções começa de cima para baixo. Quando uma função é atendida pelo pattern matching ela não vai para a próxima.

def render(_id, :txt), do: # ...

def render(_id, type), do: # Ela irá parar aqui

def render(_id, :pdf), do: # Nunca tentará fazer o pattern matching

Conclusão

Pattern matching é uma das principais funcionalidades do elixir. Versátil e fácil de usar, é uma excelente ferramenta para se aprender. Coisas complexas podem ser resolvidas de forma simples e elegante.

Last updated