A correspondência de padrões é o grande novo recurso que chega ao Ruby 2.7. Foi entregue ao tronco para que qualquer pessoa interessada possa instalar Ruby 2.7.0-dev e dê uma olhada. Lembre-se de que nada disso foi finalizado e que a equipe de desenvolvimento está procurando feedback, portanto, se você tiver algum, informe os responsáveis pelo commit antes que o recurso seja lançado.
Espero que você entenda o que é correspondência de padrões e como usá-la em Ruby depois de ler este artigo.
A correspondência de padrões é um recurso comumente encontrado em linguagens de programação funcionais. De acordo com Escala de documentação , 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 string ou reconhecimento de padrão. A correspondência de padrões não tem nada a ver com string, mas sim com a estrutura de dados. A primeira vez que encontrei correspondência de padrões foi há cerca de dois anos, quando experimentei Elixir . Eu estava aprendendo Elixir e tentando resolver algoritmos com ele. Eu 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 combinação 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 um 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 naquele í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 sendo atribuídos, MatchError
é gerado em vez disso. 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 da chave name
é Zote
e vincula o valor da chave title
ao título da variável.
Este conceito funciona muito bem quando a estrutura de dados é complexa. Você pode atribuir sua variável e verificar se há valores ou tipos em uma linha.
Além disso, também permite que uma linguagem digitada dinamicamente como Elixir tenha sobrecarga de método:
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, métodos diferentes são executados.
Esperançosamente, isso mostra como a correspondência de padrões pode ser poderosa. Existem muitas tentativas de trazer correspondência de padrões para Ruby com joias como Noaidi , qo , e egison-ruby .
Ruby 2.7 também tem sua própria implementação não muito diferente dessas joias, e é assim que está sendo feito atualmente.
A correspondência de padrões em Ruby é feita por meio de um case
declaração. No entanto, em vez de usar o usual when
, a palavra-chave in
é usado em seu lugar. Ele também suporta o uso de if
ou unless
afirmações:
case [variable or expression] in [pattern] ... in [pattern] if [expression] ... else ... end
A declaração de caso pode aceitar uma variável ou uma expressão e isso será comparado com os padrões fornecidos no dentro cláusula. E se ou a menos que instruções também podem ser fornecidas após o padrão. A verificação de igualdade aqui também usa ===
como a instrução de caso normal. Isso significa que você pode combinar subconjuntos e instâncias de classes. Aqui está um exemplo de como você o usa:
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 variável translation
é comparado 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 no translation
variável em cada um dos índices. Se os valores corresponderem, ele atribuirá os valores em translation
variável para as variáveis no padrão em cada um dos índices.
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, o translation
variável agora é um hash. Ele é comparado com outro hash no in
cláusula. O que acontece é que a instrução case verifica se todas as chaves no padrão correspondem às chaves no translation
variável. Ele também verifica se todos os valores de cada chave correspondem. Em seguida, atribui os valores à variável no hash.
tamanhos de tela padrão para design responsivo
A verificação de qualidade usada na correspondência de padrões segue a lógica de ===
.
|
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, o translation
a variável é igual a {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt}
hash e ['th', orig_text, 'ja', trans_text]
array.
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.
Nesse caso, =>
pode ser usado para atribuir um 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 os valores dentro da estrutura de dados, mas também vincular esses valores a uma variável.
Aqui, o operador pin impede 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 a 1, 2 e então 2. Ela será atribuída a 1, a 2 e a 2. Esta não é uma situação ideal se você quiser verificar se todos os os valores na matriz são iguais.
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 é comparado com 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, é correspondido 2 no terceiro índice para que 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.
como o python é usado em finanças
_
)O sublinhado (_
) é usado para ignorar os valores. Vejamos 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 _
passes. No segundo caso, =>
operador captura o valor que foi ignorado também.
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:
É 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 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 eu 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 torna mais fácil visualizar quais dados esperar:
`{nickname: nickname, realname: {first: first, last: last}}`
Em vez de:
`name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last]`.
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 a uma matriz ou hash, deconstruct
ou deconstruct_keys
são chamados, respectivamente.
Os resultados desses métodos serão usados para comparar 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
. Possui xey como atributos. Ele também tem deconstruct
e deconstruct_keys
métodos definidos.
c = Coordinates.new(32,50) case c in [a,b] p a #=> 32 p b #=> 50 end
Aqui, uma instância de Coordinate
está sendo definido e o padrão combinado com 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:} p x #=> 32 p y #=> 50 end
Neste exemplo, a mesma instância de Coordinate
está sendo correspondido por padrão a um hash. Nesse caso, o Coordinate#deconstruct_keys
resultado é usado para comparar com o hash {x: x, y: y}
definido no padrão.
Tendo experimentado a correspondência de padrões pela primeira vez no Elixir, pensei que esse recurso poderia incluir sobrecarga de método e implementado com uma sintaxe que requer apenas uma linha. No entanto, Ruby não é uma linguagem construída com o casamento 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 é realmente semelhante ao da implementação de correspondência de padrões do Scala.
Pessoalmente, acho que a correspondência de padrões é um recurso novo e interessante 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 excitante. Eu adoraria ver o que as pessoas pensam disso e como esse recurso evoluirá no futuro.
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. ”
Ele tem o potencial de tornar o código muito mais limpo e fazer o Ruby parecer um pouco mais moderno e excitante.
Por meio da instrução switch case em Ruby 2.7, usando a palavra-chave 'in' em vez do usual 'when'.
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. ”