Uma variedade de possibilidades: um guia para correspondência de padrões Ruby

Publicados: 2022-03-11

A correspondência de padrões é o grande novo recurso do Ruby 2.7. Ele foi comprometido com o tronco para que qualquer pessoa interessada possa instalar o Ruby 2.7.0-dev e conferir. Por favor, tenha em mente que nenhum deles está finalizado e a equipe de desenvolvimento está procurando por feedback, então se você tiver algum, você pode informar os committers antes que o recurso seja realmente lançado.

Espero que você entenda o que é correspondência de padrões e como usá-la em Ruby depois de ler este artigo.

O que é correspondência de padrões?

A correspondência de padrões é um recurso comumente encontrado em linguagens de programação funcionais. De acordo com a documentação do Scala, a correspondência de padrões é “um mecanismo para verificar um valor em relação a um padrão. Uma combinação bem-sucedida também pode desconstruir um valor em suas partes constituintes.”

Isso não deve ser confundido com Regex, correspondência de strings ou reconhecimento de padrões. A correspondência de padrões não tem nada a ver com string, mas sim com estrutura de dados. A primeira vez que encontrei a correspondência de padrões foi há cerca de dois anos, quando experimentei o Elixir. Eu estava aprendendo Elixir e tentando resolver algoritmos com ele. Comparei minha solução com outras e percebi que eles usavam correspondência de padrões, o que tornava seu código muito mais sucinto e fácil de ler.

Por causa disso, a correspondência de padrões realmente me impressionou. É assim que a correspondência de padrões no Elixir se parece:

 [a, b, c] = [:hello, "world", 42] a #=> :hello b #=> "world" c #=> 42

O exemplo acima se parece muito com uma atribuição múltipla em Ruby. No entanto, é mais do que isso. Ele também verifica se os valores correspondem ou não:

 [a, b, 42] = [:hello, "world", 42] a #=> :hello b #=> "world"

Nos exemplos acima, o número 42 no lado esquerdo não é uma variável que está sendo atribuída. É um valor para verificar se o mesmo elemento nesse índice específico corresponde ao do lado direito.

 [a, b, 88] = [:hello, "world", 42] ** (MatchError) no match of right hand side value

Neste exemplo, em vez dos valores serem atribuídos, MatchError é gerado. Isso ocorre porque o número 88 não corresponde ao número 42.

Também funciona com mapas (que é semelhante ao hash em Ruby):

 %{"name": "Zote", "title": title } = %{"name": "Zote", "title": "The mighty"} title #=> The mighty

O exemplo acima verifica se o valor do name da chave é Zote e vincula o valor do title da chave à variável title.

Este conceito funciona muito bem quando a estrutura de dados é complexa. Você pode atribuir sua variável e verificar valores ou tipos em uma linha.

Além disso, também permite que uma linguagem tipada dinamicamente como Elixir tenha sobrecarga de métodos:

 def process(%{"animal" => animal}) do IO.puts("The animal is: #{animal}") end def process(%{"plant" => plant}) do IO.puts("The plant is: #{plant}") end def process(%{"person" => person}) do IO.puts("The person is: #{person}") end

Dependendo da chave do hash do argumento, diferentes métodos são executados.

Felizmente, isso mostra o quão poderosa a correspondência de padrões pode ser. Existem muitas tentativas de trazer a correspondência de padrões para Ruby com gems como noaidi, qo e egison-ruby.

Ruby 2.7 também tem sua própria implementação não muito diferente dessas gems, e é assim que está sendo feito atualmente.

Sintaxe de correspondência de padrões Ruby

A correspondência de padrões em Ruby é feita por meio de uma instrução case . No entanto, em vez de usar o usual when , a palavra-chave in é usada. Ele também suporta o uso de instruções if ou unless :

 case [variable or expression] in [pattern] ... in [pattern] if [expression] ... else ... end

A instrução case pode aceitar uma variável ou uma expressão e isso será comparado com os padrões fornecidos na cláusula in . As instruções se ou a menos que também podem ser fornecidas após o padrão. A verificação de igualdade aqui também usa === como a instrução case normal. Isso significa que você pode combinar subconjuntos e instâncias de classes. Aqui está um exemplo de como você usa:

Matrizes correspondentes

 translation = ['th', 'เต้', 'ja', 'テイ'] case translation in ['th', orig_text, 'en', trans_text] puts "English translation: #{orig_text} => #{trans_text}" in ['th', orig_text, 'ja', trans_text] # this will get executed puts "Japanese translation: #{orig_text} => #{trans_text}" end

No exemplo acima, a translation da variável é comparada com dois padrões:

['th', orig_text, 'en', trans_text] e ['th', orig_text, 'ja', trans_text] . O que ele faz é verificar se os valores no padrão correspondem aos valores na variável de translation em cada um dos índices. Se os valores corresponderem, ele atribui os valores na variável de translation às variáveis ​​no padrão em cada um dos índices.

Animação de correspondência de padrões Ruby: matrizes correspondentes

Hashes correspondentes

 translation = {orig_lang: 'th', trans_lang: 'en', orig_txt: 'เต้', trans_txt: 'tae' } case translation in {orig_lang: 'th', trans_lang: 'en', orig_txt: orig_txt, trans_txt: trans_txt} puts "#{orig_txt} => #{trans_txt}" end

No exemplo acima, a variável de translation agora é um hash. Ele é comparado com outro hash na cláusula in . O que acontece é que a instrução case verifica se todas as chaves no padrão correspondem às chaves na variável de translation . Ele também verifica se todos os valores de cada chave correspondem. Em seguida, ele atribui os valores à variável no hash.

Animação de correspondência de padrões Ruby: matrizes correspondentes

Subconjuntos correspondentes

A verificação de qualidade usada na correspondência de padrões segue a lógica de === .

Vários padrões

  • | pode ser usado para definir vários padrões para um bloco.
 translation = ['th', 'เต้', 'ja', 'テイ'] case array in {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} | ['th', orig_text, 'ja', trans_text] puts orig_text #=> เต้ puts trans_text #=> テイend

No exemplo acima, a variável de translation corresponde ao {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} e ['th', orig_text, 'ja', trans_text] variedade.

Isso é útil quando você tem tipos ligeiramente diferentes de estruturas de dados que representam a mesma coisa e deseja que ambas as estruturas de dados executem o mesmo bloco de código.

Atribuição de Setas

Nesse caso, => pode ser usado para atribuir o valor correspondente a uma variável.

 case ['I am a string', 10] in [Integer, Integer] => a # not reached in [String, Integer] => b puts b #=> ['I am a string', 10] end

Isso é útil quando você deseja verificar valores dentro da estrutura de dados, mas também vincula esses valores a uma variável.

Operador de pinos

Aqui, o operador pin evita que as variáveis ​​sejam reatribuídas.

 case [1,2,2] in [a,a,a] puts a #=> 2 end

No exemplo acima, a variável a no padrão é comparada com 1, 2 e depois 2. Ela será atribuída a 1, depois a 2 e depois a 2. Esta não é uma situação ideal se você quiser verificar se todos os os valores na matriz são os mesmos.

 case [1,2,2] in [a,^a,^a] # not reached in [a,b,^b] puts a #=> 1 puts b #=> 2 end

Quando o operador pin é usado, ele avalia a variável em vez de reatribuí-la. No exemplo acima, [1,2,2] não corresponde a [a,^a,^a] porque no primeiro índice, a é atribuído a 1. No segundo e terceiro, a é avaliado como 1, mas é combinado contra 2.

No entanto, [a,b,^b] corresponde a [1,2,2] uma vez que a é atribuído a 1 no primeiro índice, b é atribuído a 2 no segundo índice, então ^b, que agora é 2, é comparado com 2 no terceiro índice para que ele passe.

 a = 1 case [2,2] in [^a,^a] #=> not reached in [b,^b] puts b #=> 2 end

Variáveis ​​de fora da instrução case também podem ser usadas conforme mostrado no exemplo acima.

Operador de sublinhado ( _ )

O sublinhado ( _ ) é usado para ignorar valores. Vejamos em alguns exemplos:

 case ['this will be ignored',2] in [_,a] puts a #=> 2 end
 case ['a',2] in [_,a] => b puts a #=> 2 Puts b #=> ['a',2] end

Nos dois exemplos acima, qualquer valor que corresponda a _ passa. Na instrução do segundo caso, o operador => captura o valor que também foi ignorado.

Casos de uso para correspondência de padrões em Ruby

Imagine que você tenha os seguintes dados JSON:

 { nickName: 'Tae' realName: {firstName: 'Noppakun', lastName: 'Wongsrinoppakun'} username: 'tae8838' }

Em seu projeto Ruby, você deseja analisar esses dados e exibir o nome com as seguintes condições:

  1. Se o nome de usuário existir, retorne o nome de usuário.
  2. Se o apelido, o nome e o sobrenome existirem, retorne o apelido, o nome e, em seguida, o sobrenome.
  3. Se o apelido não existir, mas o nome e o sobrenome sim, retorne o nome e depois o sobrenome.
  4. Se nenhuma das condições se aplicar, retorne “Novo Usuário”.

É assim que eu escreveria este programa em Ruby agora:

 def display_name(name_hash) if name_hash[:username] name_hash[:username] elsif name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last] "#{name_hash[:nickname]} #{name_hash[:realname][:first]} #{name_hash[:realname][:last]}" elsif name_hash[:first] && name_hash[:last] "#{name_hash[:first]} #{name_hash[:last]}" else 'New User' end end

Agora, vamos ver como fica com a correspondência de padrões:

 def display_name(name_hash) case name_hash in {username: username} username in {nickname: nickname, realname: {first: first, last: last}} "#{nickname} #{first} #{last}" in {first: first, last: last} "#{first} #{last}" else 'New User' end end

A preferência de sintaxe pode ser um pouco subjetiva, mas prefiro a versão de correspondência de padrões. Isso ocorre porque a correspondência de padrões nos permite escrever o hash que esperamos, em vez de descrever e verificar os valores do hash. Isso facilita a visualização de quais dados esperar:

 `{nickname: nickname, realname: {first: first, last: last}}`

Ao invés de:

 `name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last]`.

Desconstruir e Desconstruir_chaves

Existem dois novos métodos especiais sendo introduzidos no Ruby 2.7: deconstruct e deconstruct_keys . Quando uma instância de uma classe está sendo comparada com um array ou hash, deconstruct ou deconstruct_keys são chamados, respectivamente.

Os resultados desses métodos serão usados ​​para comparar com os padrões. Aqui está um exemplo:

 class Coordinate attr_accessor :x, :y def initialize(x, y) @x = x @y = y end def deconstruct [@x, @y] end def deconstruct_key {x: @x, y: @y} end end

O código define uma classe chamada Coordinate . Tem x e y como seus atributos. Ele também tem os métodos deconstruct e deconstruct_keys definidos.

 c = Coordinates.new(32,50) case c in [a,b] pa #=> 32 pb #=> 50 end

Aqui, uma instância de Coordinate está sendo definida e o padrão corresponde a uma matriz. O que acontece aqui é que Coordinate#deconstruct é chamado e o resultado é usado para corresponder ao array [a,b] definido no padrão.

 case c in {x:, y:} px #=> 32 py #=> 50 end

Neste exemplo, a mesma instância de Coordinate está sendo comparada com um hash. Nesse caso, o resultado Coordinate#deconstruct_keys é usado para corresponder ao hash {x: x, y: y} definido no padrão.

Um Emocionante Recurso Experimental

Tendo experimentado pela primeira vez a correspondência de padrões no Elixir, pensei que esse recurso poderia incluir sobrecarga de métodos e implementado com uma sintaxe que requer apenas uma linha. No entanto, Ruby não é uma linguagem construída com a correspondência de padrões em mente, então isso é compreensível.

Usar uma instrução case é provavelmente uma maneira muito enxuta de implementar isso e também não afeta o código existente (além dos métodos deconstruct e deconstruct_keys ). O uso da instrução case é, na verdade, semelhante ao da implementação de correspondência de padrões de Scala.

Pessoalmente, acho que a correspondência de padrões é um novo recurso empolgante para desenvolvedores Ruby. Ele tem o potencial de tornar o código muito mais limpo e fazer o Ruby parecer um pouco mais moderno e empolgante. Eu adoraria ver o que as pessoas acham disso e como esse recurso evolui no futuro.