Una matriz de posibilidades: una guía para la coincidencia de patrones Ruby
Publicado: 2022-03-11La coincidencia de patrones es la gran característica nueva que llega a Ruby 2.7. Se ha comprometido con el tronco para que cualquiera que esté interesado pueda instalar Ruby 2.7.0-dev y comprobarlo. Tenga en cuenta que ninguno de estos está finalizado y el equipo de desarrollo está buscando comentarios, por lo que si tiene alguno, puede informar a los encargados de confirmar antes de que la función esté realmente disponible.
Espero que comprenda qué es la coincidencia de patrones y cómo usarla en Ruby después de leer este artículo.
¿Qué es la coincidencia de patrones?
La coincidencia de patrones es una característica que se encuentra comúnmente en los lenguajes de programación funcionales. De acuerdo con la documentación de Scala, la coincidencia de patrones es “un mecanismo para comparar un valor con un patrón. Una combinación exitosa también puede deconstruir un valor en sus partes constituyentes”.
Esto no debe confundirse con Regex, coincidencia de cadenas o reconocimiento de patrones. La coincidencia de patrones no tiene nada que ver con la cadena, sino con la estructura de datos. La primera vez que me encontré con la coincidencia de patrones fue hace unos dos años cuando probé Elixir. Estaba aprendiendo Elixir y tratando de resolver algoritmos con él. Comparé mi solución con otras y me di cuenta de que usaban la coincidencia de patrones, lo que hizo que su código fuera mucho más breve y fácil de leer.
Por eso, la combinación de patrones realmente me impresionó. Así es como se ve la coincidencia de patrones en Elixir:
[a, b, c] = [:hello, "world", 42] a #=> :hello b #=> "world" c #=> 42
El ejemplo anterior se parece mucho a una asignación múltiple en Ruby. Sin embargo, es más que eso. También comprueba si los valores coinciden o no:
[a, b, 42] = [:hello, "world", 42] a #=> :hello b #=> "world"
En los ejemplos anteriores, el número 42 en el lado izquierdo no es una variable que se esté asignando. Es un valor verificar que el mismo elemento en ese índice en particular coincida con el del lado derecho.
[a, b, 88] = [:hello, "world", 42] ** (MatchError) no match of right hand side value
En este ejemplo, en lugar de asignar los valores, se MatchError
. Esto se debe a que el número 88 no coincide con el número 42.
También funciona con mapas (que es similar al hash en Ruby):
%{"name": "Zote", "title": title } = %{"name": "Zote", "title": "The mighty"} title #=> The mighty
El ejemplo anterior comprueba que el valor del name
de la clave es Zote
y vincula el valor del título de la clave al title
de la variable.
Este concepto funciona muy bien cuando la estructura de datos es compleja. Puede asignar su variable y buscar valores o tipos, todo en una línea.
Además, también permite que un lenguaje escrito dinámicamente como Elixir tenga una 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
Dependiendo de la clave del hash del argumento, se ejecutan diferentes métodos.
Con suerte, eso le muestra cuán poderosa puede ser la coincidencia de patrones. Hay muchos intentos de llevar la coincidencia de patrones a Ruby con gemas como noaidi, qo y egison-ruby.
Ruby 2.7 también tiene su propia implementación no muy diferente de estas gemas, y así es como se está haciendo actualmente.
Sintaxis de coincidencia de patrones de Ruby
La coincidencia de patrones en Ruby se realiza a través de una declaración de case
. Sin embargo, en lugar de usar el when
habitual, se usa la palabra clave in
. También admite el uso de declaraciones if
o unless
que:
case [variable or expression] in [pattern] ... in [pattern] if [expression] ... else ... end
La declaración de caso puede aceptar una variable o una expresión y esto se comparará con los patrones proporcionados en la cláusula in . Si o a menos que las declaraciones también se pueden proporcionar después del patrón. La verificación de igualdad aquí también usa ===
como la declaración de caso normal. Esto significa que puede hacer coincidir subconjuntos e instancias de clases. Aquí hay un ejemplo de cómo lo usas:
Matrices coincidentes
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
En el ejemplo anterior, la translation
variable se compara con dos patrones:
['th', orig_text, 'en', trans_text]
y ['th', orig_text, 'ja', trans_text]
. Lo que hace es comprobar si los valores del patrón coinciden con los valores de la variable de translation
en cada uno de los índices. Si los valores coinciden, asigna los valores de la variable de translation
a las variables del patrón en cada uno de los índices.
Hashes coincidentes
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
En el ejemplo anterior, la variable de translation
ahora es un hash. Se compara con otro hash en la cláusula in
. Lo que sucede es que la declaración del caso comprueba si todas las claves del patrón coinciden con las claves de la variable de translation
. También comprueba que todos los valores de cada clave coincidan. Luego asigna los valores a la variable en el hash.
Subconjuntos coincidentes
El control de calidad utilizado en la coincidencia de patrones sigue la lógica de ===
.
Múltiples patrones
-
|
se puede utilizar para definir múltiples patrones para un bloque.
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
En el ejemplo anterior, la variable de translation
coincide tanto con el {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt}
como con el ['th', orig_text, 'ja', trans_text]
formación.

Esto es útil cuando tiene tipos ligeramente diferentes de estructuras de datos que representan lo mismo y desea que ambas estructuras de datos ejecuten el mismo bloque de código.
Asignación de flecha
En este caso, =>
se puede usar para asignar un valor coincidente a una variable.
case ['I am a string', 10] in [Integer, Integer] => a # not reached in [String, Integer] => b puts b #=> ['I am a string', 10] end
Esto es útil cuando desea verificar valores dentro de la estructura de datos pero también vincular estos valores a una variable.
Operador pin
Aquí, el operador pin evita que las variables se reasignen.
case [1,2,2] in [a,a,a] puts a #=> 2 end
En el ejemplo anterior, la variable a en el patrón se compara con 1, 2 y luego 2. Se asignará a 1, luego a 2, luego a 2. Esta no es una situación ideal si desea verificar que todos los los valores en la matriz son los mismos.
case [1,2,2] in [a,^a,^a] # not reached in [a,b,^b] puts a #=> 1 puts b #=> 2 end
Cuando se usa el operador pin, evalúa la variable en lugar de reasignarla. En el ejemplo anterior, [1,2,2] no coincide con [a,^a,^a] porque en el primer índice, a se asigna a 1. En el segundo y tercero, a se evalúa como 1, pero se compara contra 2.
Sin embargo, [a,b,^b] coincide con [1,2,2] ya que a se asigna a 1 en el primer índice, b se asigna a 2 en el segundo índice, luego ^b, que ahora es 2, se compara con 2 en el tercer índice por lo que pasa.
a = 1 case [2,2] in [^a,^a] #=> not reached in [b,^b] puts b #=> 2 end
Las variables de fuera de la declaración del caso también se pueden usar como se muestra en el ejemplo anterior.
Operador de guión bajo ( _
)
El guión bajo ( _
) se usa para ignorar valores. Veámoslo en un par de ejemplos:
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
En los dos ejemplos anteriores, cualquier valor que coincida con _
pasa. En la declaración del segundo caso, el operador =>
captura el valor que también se ha ignorado.
Casos de uso para la coincidencia de patrones en Ruby
Imagina que tienes los siguientes datos JSON:
{ nickName: 'Tae' realName: {firstName: 'Noppakun', lastName: 'Wongsrinoppakun'} username: 'tae8838' }
En su proyecto de Ruby, desea analizar estos datos y mostrar el nombre con las siguientes condiciones:
- Si el nombre de usuario existe, devuelva el nombre de usuario.
- Si existen el apodo, el nombre y el apellido, devuelva el apodo, el nombre y luego el apellido.
- Si el apodo no existe, pero sí el nombre y el apellido, devuelva el nombre y luego el apellido.
- Si no se aplica ninguna de las condiciones, devuelva "Usuario nuevo".
Así es como escribiría este programa en Ruby ahora mismo:
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
Ahora, veamos cómo se ve con la coincidencia de patrones:
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
La preferencia de sintaxis puede ser un poco subjetiva, pero prefiero la versión de coincidencia de patrones. Esto se debe a que la coincidencia de patrones nos permite escribir el hash que esperamos, en lugar de describir y verificar los valores del hash. Esto hace que sea más fácil visualizar qué datos esperar:
`{nickname: nickname, realname: {first: first, last: last}}`
En lugar de:
`name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last]`.
Deconstruir y Deconstruir_claves
Hay dos nuevos métodos especiales que se están introduciendo en Ruby 2.7: deconstruct
y deconstruct_keys
. Cuando una instancia de una clase se compara con una matriz o hash, se llama a deconstruct
o deconstruct_keys
, respectivamente.
Los resultados de estos métodos se utilizarán para compararlos con patrones. Aquí hay un ejemplo:
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
El código define una clase llamada Coordinate
. Tiene x e y como sus atributos. También tiene definidos los métodos deconstruct
y deconstruct_keys
.
c = Coordinates.new(32,50) case c in [a,b] pa #=> 32 pb #=> 50 end
Aquí, se define una instancia de Coordinate
y se compara el patrón con una matriz. Lo que sucede aquí es que se llama a Coordinate#deconstruct
y el resultado se usa para compararlo con la matriz [a,b]
definida en el patrón.
case c in {x:, y:} px #=> 32 py #=> 50 end
En este ejemplo, la misma instancia de Coordinate
se compara con un patrón con un hash. En este caso, el resultado de Coordinate#deconstruct_keys
se usa para compararlo con el hash {x: x, y: y}
definido en el patrón.
Una característica experimental emocionante
Habiendo experimentado por primera vez la coincidencia de patrones en Elixir, pensé que esta característica podría incluir la sobrecarga de métodos e implementarse con una sintaxis que solo requiere una línea. Sin embargo, Ruby no es un lenguaje creado teniendo en cuenta la coincidencia de patrones, por lo que es comprensible.
Usar una declaración de caso es probablemente una forma muy sencilla de implementar esto y tampoco afecta el código existente (aparte de los métodos deconstruct
y deconstruct_keys
). El uso de la sentencia case es similar al de la implementación de coincidencia de patrones de Scala.
Personalmente, creo que la coincidencia de patrones es una característica nueva y emocionante para los desarrolladores de Ruby. Tiene el potencial de hacer que el código sea mucho más limpio y hacer que Ruby se sienta un poco más moderno y emocionante. Me encantaría ver qué piensa la gente de esto y cómo evoluciona esta función en el futuro.