Una serie di possibilità: una guida alla corrispondenza dei modelli Ruby
Pubblicato: 2022-03-11Il pattern matching è la grande novità in arrivo in Ruby 2.7. È stato assegnato al tronco, quindi chiunque sia interessato può installare Ruby 2.7.0-dev e verificarlo. Tieni presente che nessuno di questi è stato finalizzato e il team di sviluppo è alla ricerca di feedback, quindi se ne hai, puoi informare i committenti prima che la funzione sia effettivamente disponibile.
Spero che tu capisca cos'è il pattern matching e come usarlo in Ruby dopo aver letto questo articolo.
Che cos'è la corrispondenza dei modelli?
Il pattern matching è una caratteristica che si trova comunemente nei linguaggi di programmazione funzionale. Secondo la documentazione di Scala, il pattern matching è “un meccanismo per controllare un valore rispetto a un pattern. Una corrispondenza di successo può anche scomporre un valore nelle sue parti costitutive”.
Questo non deve essere confuso con Regex, corrispondenza di stringhe o riconoscimento di modelli. La corrispondenza dei modelli non ha nulla a che fare con la stringa, ma invece con la struttura dei dati. La prima volta che ho riscontrato il pattern matching è stato circa due anni fa, quando ho provato Elixir. Stavo imparando Elisir e cercavo di risolvere algoritmi con esso. Ho confrontato la mia soluzione con altre e mi sono reso conto che utilizzavano la corrispondenza dei modelli, il che rendeva il loro codice molto più conciso e più facile da leggere.
Per questo motivo, il pattern matching mi ha davvero impressionato. Ecco come appare la corrispondenza dei modelli in Elisir:
[a, b, c] = [:hello, "world", 42] a #=> :hello b #=> "world" c #=> 42
L'esempio sopra assomiglia molto a un incarico multiplo in Ruby. Tuttavia, è più di questo. Controlla anche se i valori corrispondono o meno:
[a, b, 42] = [:hello, "world", 42] a #=> :hello b #=> "world"
Negli esempi precedenti, il numero 42 sul lato sinistro non è una variabile assegnata. È un valore per verificare che lo stesso elemento in quel particolare indice corrisponda a quello del lato destro.
[a, b, 88] = [:hello, "world", 42] ** (MatchError) no match of right hand side value
In questo esempio, invece dei valori assegnati, viene generato MatchError
. Questo perché il numero 88 non corrisponde al numero 42.
Funziona anche con le mappe (che è simile all'hash in Ruby):
%{"name": "Zote", "title": title } = %{"name": "Zote", "title": "The mighty"} title #=> The mighty
L'esempio precedente verifica che il valore del name
della chiave sia Zote
e associa il valore del title
della chiave alla variabile title.
Questo concetto funziona molto bene quando la struttura dei dati è complessa. Puoi assegnare la tua variabile e controllare valori o tipi tutto in una riga.
Inoltre, consente anche a un linguaggio tipizzato dinamicamente come Elixir di avere un sovraccarico del metodo:
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
A seconda della chiave dell'hash dell'argomento, vengono eseguiti metodi diversi.
Si spera che questo ti mostri quanto può essere potente il pattern matching. Ci sono molti tentativi di portare il pattern matching in Ruby con gemme come noaidi, qo ed egison-ruby.
Ruby 2.7 ha anche una sua implementazione non troppo diversa da queste gemme, ed è così che viene fatto attualmente.
Sintassi di corrispondenza del modello Ruby
La corrispondenza dei modelli in Ruby viene eseguita tramite un'istruzione case
. Tuttavia, invece di utilizzare il solito when
, viene utilizzata invece la parola chiave in
. Supporta anche l'uso di istruzioni if
o unless
:
case [variable or expression] in [pattern] ... in [pattern] if [expression] ... else ... end
L'istruzione Case può accettare una variabile o un'espressione e questa verrà confrontata con i modelli forniti nella clausola in. Se o meno possono essere fornite anche le istruzioni dopo il modello. Il controllo di uguaglianza qui usa anche ===
come la normale istruzione case. Ciò significa che puoi abbinare sottoinsiemi e istanze di classi. Ecco un esempio di come lo usi:
Matrici corrispondenti
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
Nell'esempio sopra, la translation
della variabile viene confrontata con due modelli:
['th', orig_text, 'en', trans_text]
e ['th', orig_text, 'ja', trans_text]
. Quello che fa è controllare se i valori nel modello corrispondono ai valori nella variabile di translation
in ciascuno degli indici. Se i valori corrispondono, assegna i valori nella variabile di translation
alle variabili nel modello in ciascuno degli indici.
Hash corrispondenti
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
Nell'esempio sopra, la variabile di translation
è ora un hash. Viene confrontato con un altro hash in
clausola in. Quello che succede è che l'istruzione case controlla se tutte le chiavi nel modello corrispondono alle chiavi nella variabile di translation
. Verifica inoltre che tutti i valori per ciascuna chiave corrispondano. Quindi assegna i valori alla variabile nell'hash.
Sottoinsiemi corrispondenti
Il controllo di qualità utilizzato nel pattern matching segue la logica di ===
.
Modelli multipli
-
|
può essere utilizzato per definire più modelli per un blocco.
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
Nell'esempio sopra, la variabile di translation
corrisponde sia {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt}
e all'hash ['th', orig_text, 'ja', trans_text]
Vettore.

Ciò è utile quando si hanno tipi leggermente diversi di strutture dati che rappresentano la stessa cosa e si desidera che entrambe le strutture dati eseguano lo stesso blocco di codice.
Assegnazione della freccia
In questo caso, =>
può essere utilizzato per assegnare un valore abbinato a una variabile.
case ['I am a string', 10] in [Integer, Integer] => a # not reached in [String, Integer] => b puts b #=> ['I am a string', 10] end
Ciò è utile quando si desidera controllare i valori all'interno della struttura dati ma anche associare questi valori a una variabile.
Operatore Pin
Qui, l'operatore pin impedisce la riassegnazione delle variabili.
case [1,2,2] in [a,a,a] puts a #=> 2 end
Nell'esempio sopra, la variabile a nel modello viene confrontata con 1, 2 e poi 2. Verrà assegnata a 1, quindi 2, quindi a 2. Questa non è una situazione ideale se si desidera verificare che tutti i i valori nell'array sono gli stessi.
case [1,2,2] in [a,^a,^a] # not reached in [a,b,^b] puts a #=> 1 puts b #=> 2 end
Quando viene utilizzato l'operatore pin, valuta la variabile invece di riassegnarla. Nell'esempio sopra, [1,2,2] non corrisponde a [a,^a,^a] perché nel primo indice a è assegnato a 1. Nel secondo e terzo, a viene valutato come 1, ma viene confrontato con 2.
Tuttavia [a,b,^b] corrisponde a [1,2,2] poiché a è assegnato a 1 nel primo indice, b è assegnato a 2 nel secondo indice, quindi ^b, che ora è 2, viene confrontato con 2 nel terzo indice quindi passa.
a = 1 case [2,2] in [^a,^a] #=> not reached in [b,^b] puts b #=> 2 end
È anche possibile utilizzare variabili esterne all'istruzione case, come mostrato nell'esempio precedente.
Operatore di sottolineatura ( _
).
Il trattino basso ( _
) viene utilizzato per ignorare i valori. Vediamolo in un paio di esempi:
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
Nei due esempi precedenti, qualsiasi valore che corrisponde a _
passa. Nella seconda istruzione case, l'operatore =>
cattura anche il valore che è stato ignorato.
Casi d'uso per la corrispondenza dei modelli in Ruby
Immagina di avere i seguenti dati JSON:
{ nickName: 'Tae' realName: {firstName: 'Noppakun', lastName: 'Wongsrinoppakun'} username: 'tae8838' }
Nel tuo progetto Ruby, vuoi analizzare questi dati e visualizzare il nome con le seguenti condizioni:
- Se il nome utente esiste, restituire il nome utente.
- Se il soprannome, il nome e il cognome esistono, restituire il soprannome, il nome e quindi il cognome.
- Se il nickname non esiste, ma il nome e il cognome esistono, restituisci il nome e poi il cognome.
- Se nessuna delle condizioni si applica, restituisci "Nuovo utente".
Ecco come scriverei questo programma in Ruby in questo momento:
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
Ora, vediamo come appare con la corrispondenza del modello:
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 preferenza della sintassi può essere un po' soggettiva, ma preferisco la versione di corrispondenza dei modelli. Questo perché il pattern matching ci consente di scrivere l'hash che ci aspettiamo, invece di descrivere e controllare i valori dell'hash. In questo modo è più facile visualizzare quali dati aspettarsi:
`{nickname: nickname, realname: {first: first, last: last}}`
Invece di:
`name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last]`.
Deconstruct e Deconstruct_keys
Ci sono due nuovi metodi speciali introdotti in Ruby 2.7: deconstruct
e deconstruct_keys
. Quando un'istanza di una classe viene confrontata con un array o un hash, vengono chiamati rispettivamente deconstruct
o deconstruct_keys
.
I risultati di questi metodi verranno utilizzati per confrontare i modelli. Ecco un esempio:
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
Il codice definisce una classe chiamata Coordinate
. Ha xey come suoi attributi. Ha anche i metodi deconstruct
e deconstruct_keys
definiti.
c = Coordinates.new(32,50) case c in [a,b] pa #=> 32 pb #=> 50 end
Qui, viene definita un'istanza di Coordinate
e il modello viene confrontato con un array. Quello che succede qui è che Coordinate#deconstruct
viene chiamato e il risultato viene utilizzato per confrontare l'array [a,b]
definito nel modello.
case c in {x:, y:} px #=> 32 py #=> 50 end
In questo esempio, la stessa istanza di Coordinate
viene confrontata con un hash. In questo caso, il risultato Coordinate#deconstruct_keys
viene utilizzato per confrontare l'hash {x: x, y: y}
definito nel modello.
Un'emozionante caratteristica sperimentale
Avendo sperimentato per la prima volta la corrispondenza dei modelli in Elixir, avevo pensato che questa funzionalità potesse includere il sovraccarico del metodo e implementata con una sintassi che richiede solo una riga. Tuttavia, Ruby non è un linguaggio creato pensando alla corrispondenza dei modelli, quindi è comprensibile.
L'uso di un'istruzione case è probabilmente un modo molto snello di implementarlo e inoltre non influisce sul codice esistente (a parte i metodi deconstruct
e deconstruct_keys
). L'uso dell'istruzione case è in realtà simile a quello dell'implementazione del pattern matching da parte di Scala.
Personalmente, penso che il pattern matching sia una nuova entusiasmante funzionalità per gli sviluppatori di Ruby. Ha il potenziale per rendere il codice molto più pulito e far sentire Ruby un po' più moderno ed eccitante. Mi piacerebbe vedere cosa ne pensano le persone e come si evolverà questa funzione in futuro.