O Pattern Match do Elixir, só que em Ruby.

Sabemos que o Pattern Matching é uma das operações mais icônicas e importantes do Elixir, capaz de reduzir lógicas complexas, facilitar a desconstrução de dados em poucas linhas, além de vários outros benefícios. Mas será que existe essa possibilidade em Ruby? Te explicamos neste post do blog.

Pattern Match ou Pattern Matching é uma das operações mais icônicas do Elixir. E, como Phoenix e Rails são dois dos frameworks mais produtivos para criar aplicações web full-stack (|| backend), é inevitável questionar se é possível fazer a mesma operação em Ruby — especialmente se você utiliza rotineiramente essas duas linguagens.

Outros questionamentos podem surgir em relação a uma prática assim: 

  • Apesar de aparentemente possível, é viável? 
  • Qual o benefício dessa operação?
  • Quais são as limitações?
  • É uma boa prática? 
  • Vale a pena?

Neste artigo, vou abordar de forma resumida cada um desses questionamentos. Te convido a continuar a leitura:

O Operador Match

Pattern Matching é basicamente a ação de criar padrões aos quais certos dados devem se conformar (ou “se encaixar”) e, em seguida, verificar se tal conformidade com o padrão descrito ocorreu. 

Caso seja bem-sucedida, esses dados são desconstruídos e possivelmente gravados em variáveis instanciadas junto com as regras do padrão requisitado.

Pattern Matching no Elixir

Dadas as origens funcionais por trás da sintaxe Elixir, seu operador `=` — que em outras linguagens seria conhecido como uma atribuição de valor ou assignment — já é na verdade o Pattern Matching em si. 

Quando o código: `x = 1` executa, ele está na verdade verificando se `x = 1` se encaixa no padrão descrito, dada a variável livre, que nesse caso foi fornecida como regra. Caso esse equilíbrio ou encaixe entre os dois lados do operador seja possível, as variáveis presentes são então vinculadas a esses valores. 

A operação só será possível — isto é, só atribuirá qualquer valor a alguma variável — quando todas as condições de uma cláusula forem satisfeitas simultaneamente.

O exemplo a seguir mostra a maneira como o Elixir lida com um Pattern Match inválido entre dois mapas (semelhantes ao hash, em Ruby). Nesse caso, a saída será de erro. Ou seja, nenhuma das variáveis terá sido atribuída no final da comparação:

1
2
3
4
iex(1)> %{name: a, age: b } = %{name: "Carlos"}  
** (MatchError) no match of right hand side value: %{name: "Carlos"}
iex(1)> b
** (CompileError) iex:1: undefined function b/0

Em resumo, o match não aconteceu, porque o lado esquerdo requereria a presença de um atributo chamado `:age` também no lado oposto, enquanto seu único atributo era `:name`. Após isso, tentar acessar `b` resultará também em um erro, dado que o match não foi realizado.

Importante: vale mencionar que no exemplo acima, caso o lado esquerdo da expressão possuísse apenas name, enquanto o direito possuísse `age` e até mesmo quaisquer outros atributos como na sequência abaixo, o match teria sucesso. Exemplo:

1
iex(1)>  %{name: a } = %{name: "Carlos", age: 30, type: :developer}  
%{age: 30, name: "Carlos", type: :developer}
iex(2)> a
"Carlos

Isso ocorre porque a regra do match para mapas requer apenas a presença dos atributos estipulados à esquerda. A presença de atributos além dos especificados não causa nenhuma falha no match em um mapa.

Pattern Matching no Ruby 

Desde a versão 2.7, lançada no final de 2019, a linguagem Ruby agora inclui a operação de Pattern Matching. Por ser uma linguagem orientada a objetos e imperativa, seu operador `=` obviamente já está reservado para atribuição. Portanto, a sintaxe do match foi definida como algo bem próximo do case/when. 

Para o match, utiliza-se o operador case, e ao invés de when, utiliza-se in. A sintaxe resumida é, portanto, a seguinte:

case expressao
  in padrao1 # if | unless condicao
  in padrao2 # if | unless condicao
  else
end

Exemplo prático

irb(main):001:2* (case {name: 'Carlos', type: :developer, logged: false} 
irb(main):002:2*     in {name: 'Carlos', type: :developer} 
irb(main):003:2*       :success
irb(main):004:0> end)
=> :success


No exemplo acima não há nenhuma condição padrão ou fallback, pois não há else. Portanto, caso o exemplo anterior não estivesse no formato compatível com a primeira condição, o comando resultaria em erro:

irb(main):001:2* (case {name: 'Carlos', type: :developer, logged: false} 
irb(main):002:2*     in {name: 'Carlos', type: :designer} 
irb(main):003:2*       :success
irb(main):004:0> end)
=> NoMatchingPatternError ({:name=>"Carlos", :type=>:developer, :logged=>false})

Por esse motivo, recomenda-se que sempre haja uma cláusula else para algum fallback que evite erros caso o dado a ser comparado possa variar.
Agora, vamos testar utilizando guard clauses e também o caso padrão (else):

irb(main):005:0> hash = {name: 'Carlos', type: :developer, logged: false}
irb(main):014:2* (case hash 
irb(main):015:2* in {name: 'Carlos', type: :developer} if hash[:logged]
irb(main):016:2*       :success
irb(main):017:0> else :failure end)
=> :failure

Nesse caso, a saída da execução foi `:failure` porque a condição `if` dentro da cláusula que retornaria sucesso previne que o match aconteça caso não seja cumprida. Portanto, esse comportamento no Ruby é análogo ao das “Guard Clauses”, ou “Cláusulas Guarda” do Elixir.

Além do próprio hash ter que estar no padrão descrito, somente se hash[:logged] fosse truthy, isto é, true ou que avalia para true, o match teria acontecido.

Importante lembrar:

  1. A primeira cláusula a dar match sempre fará com que o case pare de executar, já que ele encontrou o match. A mesma regra se aplica ao match no Elixir, e foi absorvida também pelo Ruby para manter a consistência.
  2. Se nenhuma cláusula se encaixar no case e ele não possuir nenhuma cláusula `else`, o Ruby chegará a um NoMatchingPatternError. Lembrando que se trata realmente de um erro, o que faria com que a aplicação fechasse, ou que o servidor (Puma, por exemplo) retornasse um erro 500 - Internal Server Error.

Viabilidade e Limitações

Conforme descrito anteriormente, o Pattern Matching só está disponível a partir da versão 2.7 do Ruby, lançada ao final de 2019. Isso significa que muitos ambientes rodando a linguagem não têm e ainda não terão, num futuro próximo, suporte à sintaxe.

Isso inclui funções serverless, como Amazon Lambda e outros, ambientes on-premise cujas atualizações não podem ser tão frequentes, ambientes obrigatoriamente legados, entre outros.

Além disso, a funcionalidade ainda é realmente experimental. Tanto que a cada Pattern Match executado, o ambiente Ruby dispara a seguinte mensagem:

(irb):10: warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!

Note que, apesar de ser possível ignorar por padrão esse aviso, ele não é um dos reais transtornos que uma feature experimental pode trazer. 

A mensagem serve para alertar que ainda não foi batido o martelo definitivo sobre cada nuance das regras do match. E ter uma aplicação que pode deixar de funcionar após uma mudança sutil de runtime — lançada em uma versão menor da linguagem e que já foi sujeita a mudanças de regras entre versões menores — não parece uma boa ideia.

Como uma limitação secundária fica o fato de que, em experiências que tivemos em testes aqui na ateliware, o código Ruby construído utilizando Pattern Match não se parece, em diversas vezes, muito natural. Como a linguagem possui por padrão do operador `=` a atribuição convencional, a estrutura sintática em torno do match é significativamente maior, principalmente se o seu uso for frequente. 

O Ruby também já possuía, por si só, vários tipos de desconstrução de dados para atribuições em variáveis, além da sintaxe limpa mesmo em código excessivamente imperativo/iterativo. Por ainda não ter muitos adeptos, a sintaxe do match em Ruby ainda não é também por vezes muito clara aos desenvolvedores que não conhecem sua forma de comparação.

Conclusão: utilizar o Pattern Match em Ruby é uma boa prática? 

Dada a atual situação experimental da feature de Pattern Match no Ruby — ao menos até a criação deste artigo, em junho de 2020 — consideramos que por enquanto não é a melhor ideia utilizar o Pattern Match em Ruby como forma de tornar o código mais clean, nem para exportar código originalmente em Elixir, já que há certas diferenças em como o match se comporta em cada linguagem, além de nuances como o operador pin (^) do Elixir, que é parte do dia a dia no uso do match.

Por fim, considero que o conjunto Ruby + Rails provê tantas ferramentas e sugars para facilitar comparações lógicas envolvendo objetos de diversas classes — já que quase tudo em Ruby é objeto de alguma classe — que torna o Pattern Match um mero figurante no contexto atual da linguagem.

É possível que futuramente a versão definitiva da feature agregue em peso à sintaxe da linguagem assim como faz no Elixir, mas por hora, a recomendação sobre o uso do Pattern Matching em Ruby fica de que só seja feita se extremamente necessária, ou no âmbito dos testes e experimentação. 

Consegui te ajudar com as dúvidas que citei no começo do artigo? Se ainda restou alguma, entre em contato comigo direto nos canais sociais.

Carlos Grell

software engineer | Fascinado por tudo que envolve ciência, lógica e conhecimento. Criptografia, user interfaces, eletrônica digital, Internet das Coisas. Fã de código limpo e reutilizável.

Ícones em Interfaces Digitais

A relação do consumidor 4.0 e a transformação digital

Como escolher os melhores métodos de UX research para o seu processo?