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.

Carlos Grell

Carlos Grell

June 04, 2020 | leitura de 7 minutos

dev

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:

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:

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 com a nossa equipe através do site!

Carlos Grell
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.

LinkedInInstagramGithub