Tablica możliwości: przewodnik po dopasowywaniu wzorców Ruby
Opublikowany: 2022-03-11Dopasowywanie wzorców to duża nowa funkcja, która pojawi się w Ruby 2.7. Został on przypisany do bagażnika, więc każdy, kto jest zainteresowany, może zainstalować Ruby 2.7.0-dev i sprawdzić go. Pamiętaj, że żaden z nich nie został sfinalizowany, a zespół programistów oczekuje opinii, więc jeśli masz jakieś, możesz powiadomić osoby dokonujące zmian, zanim funkcja zostanie faktycznie wydana.
Mam nadzieję, że po przeczytaniu tego artykułu zrozumiesz, czym jest dopasowanie wzorców i jak z niego korzystać w Rubim.
Co to jest dopasowywanie wzorców?
Dopasowywanie wzorców to funkcja często spotykana w funkcjonalnych językach programowania. Zgodnie z dokumentacją Scali, dopasowanie wzorca jest „mechanizmem sprawdzania wartości względem wzorca. Udane dopasowanie może również rozłożyć wartość na części składowe.”
Nie należy tego mylić z wyrażeniem regularnym, dopasowywaniem ciągów lub rozpoznawaniem wzorców. Dopasowywanie wzorców nie ma nic wspólnego z ciągiem, ale strukturą danych. Po raz pierwszy zetknąłem się z dopasowaniem wzorców około dwa lata temu, kiedy wypróbowałem Elixir. Uczyłem się Eliksiru i próbowałem za jego pomocą rozwiązywać algorytmy. Porównałem moje rozwiązanie z innymi i zdałem sobie sprawę, że używają one dopasowywania wzorców, dzięki czemu ich kod jest o wiele bardziej zwięzły i łatwiejszy do odczytania.
Dzięki temu dopasowanie wzorów naprawdę zrobiło na mnie wrażenie. Tak wygląda dopasowanie wzorców w Elixirze:
[a, b, c] = [:hello, "world", 42] a #=> :hello b #=> "world" c #=> 42
Powyższy przykład bardzo przypomina wielokrotne przypisanie w Ruby. Jednak to coś więcej. Sprawdza również, czy wartości są zgodne:
[a, b, 42] = [:hello, "world", 42] a #=> :hello b #=> "world"
W powyższych przykładach liczba 42 po lewej stronie nie jest przypisywaną zmienną. Jest to wartość sprawdzająca, czy ten sam element w tym konkretnym indeksie pasuje do tego po prawej stronie.
[a, b, 88] = [:hello, "world", 42] ** (MatchError) no match of right hand side value
W tym przykładzie zamiast przypisywanych wartości zgłaszany jest MatchError
. Dzieje się tak, ponieważ liczba 88 nie pasuje do liczby 42.
Działa również z mapami (co jest podobne do hash w Ruby):
%{"name": "Zote", "title": title } = %{"name": "Zote", "title": "The mighty"} title #=> The mighty
Powyższy przykład sprawdza, czy wartość name
klucza to Zote
i wiąże wartość tytułu klucza z title
zmiennej.
Ta koncepcja sprawdza się bardzo dobrze, gdy struktura danych jest złożona. Możesz przypisać zmienną i sprawdzić wartości lub typy w jednym wierszu.
Co więcej, umożliwia również dynamicznemu typowaniu językowi, takiemu jak Elixir, stosowanie przeciążania metod:
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
W zależności od klucza skrótu argumentu wykonywane są różne metody.
Mamy nadzieję, że pokazuje to, jak potężne może być dopasowywanie wzorców. Istnieje wiele prób wprowadzenia dopasowania wzorców do Rubiego za pomocą klejnotów, takich jak noaidi, qo i egison-ruby.
Ruby 2.7 ma również własną implementację, która nie różni się zbytnio od tych perełek i tak to się obecnie robi.
Składnia dopasowywania wzorca Ruby
Dopasowywanie wzorców w Ruby odbywa się za pomocą instrukcji case
. Jednak zamiast zwykłego when
używane jest słowo kluczowe in
. Obsługuje również użycie instrukcji if
lub unless
:
case [variable or expression] in [pattern] ... in [pattern] if [expression] ... else ... end
Instrukcja Case może akceptować zmienną lub wyrażenie i będzie dopasowywana do wzorców podanych w klauzuli in . Po wzorcu można również podać oświadczenia „jeśli” lub „ chyba ”. Kontrola równości tutaj również używa ===
jak normalna instrukcja case. Oznacza to, że możesz dopasować podzbiory i wystąpienia klas. Oto przykład, jak z niego korzystasz:
Pasujące tablice
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
W powyższym przykładzie translation
zmiennej jest dopasowywane do dwóch wzorców:
['th', orig_text, 'en', trans_text]
i ['th', orig_text, 'ja', trans_text]
. To, co robi, to sprawdzenie, czy wartości we wzorcu odpowiadają wartościom w zmiennej translation
w każdym z indeksów. Jeśli wartości są zgodne, przypisuje wartości w zmiennej translation
do zmiennych we wzorcu w każdym z indeksów.
Dopasowanie haszy
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
W powyższym przykładzie zmienna translation
jest teraz hashem. Jest dopasowywany do innego skrótu in
. Dzieje się tak, że instrukcja case sprawdza, czy wszystkie klucze we wzorcu pasują do kluczy w zmiennej translation
. Sprawdza również, czy wszystkie wartości dla każdego klucza są zgodne. Następnie przypisuje wartości do zmiennej w hashu.
Pasujące podzbiory
Kontrola jakości używana w dopasowywaniu wzorców jest zgodna z logiką ===
.
Wiele wzorów
-
|
może służyć do definiowania wielu wzorów dla jednego bloku.
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
W powyższym przykładzie zmienna translation
jest zgodna zarówno z {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt}
, jak i ['th', orig_text, 'ja', trans_text]
szyk.

Jest to przydatne, gdy masz nieco inne typy struktur danych, które reprezentują to samo i chcesz, aby obie struktury danych wykonywały ten sam blok kodu.
Przypisanie strzałki
W takim przypadku =>
może służyć do przypisania dopasowanej wartości do zmiennej.
case ['I am a string', 10] in [Integer, Integer] => a # not reached in [String, Integer] => b puts b #=> ['I am a string', 10] end
Jest to przydatne, gdy chcesz sprawdzić wartości w strukturze danych, ale także powiązać te wartości ze zmienną.
Operator przypinania
W tym przypadku operator pin zapobiega ponownemu przypisaniu zmiennych.
case [1,2,2] in [a,a,a] puts a #=> 2 end
W powyższym przykładzie zmienna a we wzorcu jest dopasowywana do 1, 2, a następnie 2. Zostanie przypisana do 1, potem 2, potem 2. Nie jest to idealna sytuacja, jeśli chcesz sprawdzić, czy wszystkie wartości w tablicy są takie same.
case [1,2,2] in [a,^a,^a] # not reached in [a,b,^b] puts a #=> 1 puts b #=> 2 end
Gdy używany jest operator pin, ocenia zmienną zamiast ponownego jej przypisywania. W powyższym przykładzie [1,2,2] nie pasuje do [a,^a,^a], ponieważ w pierwszym indeksie a jest przypisane do 1. W drugim i trzecim a jest oceniane jako 1, ale jest dopasowany do 2.
Jednak [a,b,^b] pasuje do [1,2,2], ponieważ a jest przypisane do 1 w pierwszym indeksie, b jest przypisane do 2 w drugim indeksie, wtedy ^b, które teraz wynosi 2, jest dopasowywane do 2 w trzecim indeksie, więc mija.
a = 1 case [2,2] in [^a,^a] #=> not reached in [b,^b] puts b #=> 2 end
Można również użyć zmiennych spoza instrukcji case, jak pokazano w powyższym przykładzie.
Podkreślenie ( _
) Operator
Podkreślenie ( _
) służy do ignorowania wartości. Zobaczmy to na kilku przykładach:
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
W dwóch powyższych przykładach każda wartość pasująca do _
przechodzi. W drugim przypadku operator =>
przechwytuje również wartość, która została zignorowana.
Użyj przypadków dopasowywania wzorców w Ruby
Wyobraź sobie, że masz następujące dane JSON:
{ nickName: 'Tae' realName: {firstName: 'Noppakun', lastName: 'Wongsrinoppakun'} username: 'tae8838' }
W swoim projekcie Ruby chcesz przeanalizować te dane i wyświetlić nazwę z następującymi warunkami:
- Jeśli nazwa użytkownika istnieje, zwróć nazwę użytkownika.
- Jeśli pseudonim, imię i nazwisko istnieją, zwróć pseudonim, imię, a następnie nazwisko.
- Jeśli pseudonim nie istnieje, ale imię i nazwisko tak, zwróć imię, a następnie nazwisko.
- Jeśli żaden z warunków nie ma zastosowania, zwróć „Nowy użytkownik”.
Tak bym teraz napisał ten program w Ruby:
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
Zobaczmy teraz, jak to wygląda z dopasowywaniem wzorców:
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
Preferencje składni mogą być nieco subiektywne, ale wolę wersję dopasowującą do wzorca. Dzieje się tak, ponieważ dopasowanie wzorców pozwala nam wypisać oczekiwany hasz, zamiast opisywać i sprawdzać wartości hasza. Ułatwia to wizualizację, jakich danych można się spodziewać:
`{nickname: nickname, realname: {first: first, last: last}}`
Zamiast:
`name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last]`.
Dekonstruuj i dekonstruuj_klucze
W Ruby 2.7 wprowadzono dwie nowe metody specjalne: deconstruct
i deconstruct_keys
. Gdy instancja klasy jest dopasowywana do tablicy lub skrótu, wywoływane są odpowiednio deconstruct
lub deconstruct_keys
.
Wyniki z tych metod zostaną użyte do dopasowania do wzorców. Oto przykład:
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
Kod definiuje klasę o nazwie Coordinate
. Ma x i y jako swoje atrybuty. Ma również zdefiniowane metody deconstruct
i deconstruct_keys
.
c = Coordinates.new(32,50) case c in [a,b] pa #=> 32 pb #=> 50 end
Tutaj instancja Coordinate
jest definiowana i dopasowywana do tablicy. W tym przypadku wywoływana jest funkcja Coordinate#deconstruct
, a wynik jest używany do dopasowania do tablicy [a,b]
zdefiniowanej we wzorcu.
case c in {x:, y:} px #=> 32 py #=> 50 end
W tym przykładzie to samo wystąpienie Coordinate
jest dopasowywane do wzorca względem skrótu. W takim przypadku wynik Coordinate#deconstruct_keys
jest używany do dopasowania do skrótu {x: x, y: y}
zdefiniowanego we wzorcu.
Ekscytująca funkcja eksperymentalna
Po pierwszym doświadczeniu dopasowywania wzorców w Elixirze pomyślałem, że ta funkcja może obejmować przeciążanie metod i zaimplementować składnię, która wymaga tylko jednej linii. Jednak Ruby nie jest językiem zbudowanym z myślą o dopasowywaniu wzorców, więc jest to zrozumiałe.
Użycie instrukcji case jest prawdopodobnie bardzo oszczędnym sposobem zaimplementowania tego i również nie wpływa na istniejący kod (poza metodami deconstruct
i deconstruct_keys
). Użycie instrukcji case jest w rzeczywistości podobne do implementacji dopasowywania wzorców w Scali.
Osobiście uważam, że dopasowanie wzorców jest ekscytującą nową funkcją dla programistów Ruby. Może sprawić, że kod stanie się o wiele czystszy i sprawi, że Ruby będzie bardziej nowoczesny i ekscytujący. Chciałbym zobaczyć, co ludzie z tego myślą i jak ta funkcja będzie ewoluować w przyszłości.