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

lib/person.ex
defmodule Person do
  def create(name, genre) do
    %{
      name: name,
      genre: genre
    }
  end
end

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

Se rodarmos novamente, os testes terão passados.

mix test
...
Finished in 0.02 seconds (0.00s async, 0.02s sync)
3 tests, 0 failures

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:

  use ExUnit.Case

  test "simple tests about pattern matching" do
    # ...

    assert new_name == "My Name"
    assert new_genre == :no_binary
  end
end

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.

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

  test "simple tests about pattern matching" do
    %{
      name: new_name,
      genre: new_genre
    } = Person.create("My Name", :no_binary)

    assert new_name == "My Name"
    assert new_genre == :no_binary
  end
end

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.

$ mix test
...
Finished in 0.02 seconds (0.00s async, 0.02s sync)
3 tests, 0 failures

Outros exemplo:

iex> 10 = 10
iex> x = 1
iex> %{y: value} = %{y: 100}
iex> %{person: %{name: person_name}} = %{person: %{name": "iago"}}
iex> IO.inspet(x) 
1
iex> IO.inspet(value) 
100
iex> IO.inspet(person_name) #
iago

IO.inspect

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

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

iex> {a, b, c} = {:hello, "world"}
** (MatchError) no match of right hand side value: {:hello, "world"}

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:

test/document_test.exs
defmodule DocumentTest do
  use ExUnit.Case

  describe "show/2" do
    test "render txt" do
      id = 12353
      type = :txt

      result = Document.render(id, type)

      assert result == "Rendering txt"
    end
  end
end

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

mix test test/document_test.exs
warning: Document.render/2 is undefined (module Document is not available or is yet to be defined)
  test/document_test.exs:9: DocumentTest."test show/2 render txt"/1



  1) test show/2 render txt (DocumentTest)
     test/document_test.exs:5
     ** (UndefinedFunctionError) function Document.render/2 is undefined (module Document is not available)
     code: result = Document.render(id, type)
     stacktrace:
       Document.render(12353, :txt)
       test/document_test.exs:9: (test)


Finished in 0.02 seconds (0.00s async, 0.02s sync)
1 test, 1 failure

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

lib/document.ex
defmodule Document do
  def render(_id, _type) do
    "Rendering txt"
  end
end

Rodando o teste agora, tudo vai estar funcionando.

mix test test/document_test.exs
Compiling 1 file (.ex)
.
Finished in 0.01 seconds (0.00s async, 0.01s sync)
1 test, 0 failures

Vamos lidar agora com o PDF.

test/document_test.exs
defmodule DocumentTest do
  use ExUnit.Case

  describe "show/2" do
    # ...
    
    test "render pdf" do
      id = 12353
      type = :pdf

      result = Document.render(id, type)

      assert result == "Rendering pdf"
    end
  end
end

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

mix test test/document_test.exs
.

  1) test show/2 render pdf (DocumentTest)
     test/document_test.exs:14
     Assertion with == failed
     code:  assert result == "Rendering pdf"
     left:  "Rendering txt"
     right: "Rendering pdf"
     stacktrace:
       test/document_test.exs:20: (test)


Finished in 0.02 seconds (0.00s async, 0.02s sync)
2 tests, 1 failure

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:

defmodule Document do
  def render(_id, _type) do
    if type == :txt do
      "Rendering txt"
    else
      "Rendering pdf"
    end
  end
end

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.

lib/document.ex
defmodule Document do
  def render(_id, :txt) do
    "Rendering txt"
  end

  def render(_id, :pdf) do
    "Rendering txt"
  end
end

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.

mix test test/document_test.exs
Compiling 1 file (.ex)
..
Finished in 0.01 seconds (0.00s async, 0.01s sync)
2 tests, 0 failures

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:

test/document_test.exs
defmodule DocumentTest do
  use ExUnit.Case

  describe "render/2" do
    # ...
    
    test "unsupported type" do
      id = 12353
      type = :unsupported

      result = Document.render(id, type)

      assert result == "This type #{type} is not supported"
    end
  end
end

executando o teste, temos o seguinte relatório:

mix test test/document_test.exs


  1) test show/2 unsupported type (DocumentTest)
     test/document_test.exs:23
     ** (FunctionClauseError) no function clause matching in Document.render/2

     The following arguments were given to Document.render/2:
     
         # 1
         12353
     
         # 2
         :unsupported
     
     Attempted function clauses (showing 2 out of 2):
     
         def render(_id, :txt)
         def render(_id, :pdf)
     
     code: result = Document.render(id, type)
     stacktrace:
       (hello_world 0.1.0) lib/document.ex:2: Document.render/2
       test/document_test.exs:27: (test)

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

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

lib/document.ex
defmodule Document do
  def render(_id, :txt) do
    "Rendering txt"
  end

  def render(_id, :pdf) do
    "Rendering pdf"
  end

  def render(_id, unsupported) do
    "This type #{unsupported} is not supported"
  end
end

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:

mix test test/document_test.exs
...
Finished in 0.02 seconds (0.00s async, 0.02s sync)
3 tests, 0 failures

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