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 | 4 de junho de 2020

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.

  • Leia também: Linguagem de programação Elixir, para que serve?
  • 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
    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.