Eine Reihe von Möglichkeiten: Ein Leitfaden zum Abgleich von Ruby-Mustern

Veröffentlicht: 2022-03-11

Pattern Matching ist das große neue Feature von Ruby 2.7. Es wurde in den Trunk übernommen, sodass jeder, der daran interessiert ist, Ruby 2.7.0-dev installieren und ausprobieren kann. Bitte denken Sie daran, dass keines davon abgeschlossen ist und das Entwicklerteam nach Feedback sucht. Wenn Sie also Feedback haben, können Sie es den Committern mitteilen, bevor das Feature tatsächlich veröffentlicht wird.

Ich hoffe, Sie werden nach dem Lesen dieses Artikels verstehen, was Pattern Matching ist und wie man es in Ruby verwendet.

Was ist Musterabgleich?

Der Musterabgleich ist eine Funktion, die häufig in funktionalen Programmiersprachen zu finden ist. Laut Scala-Dokumentation ist der Musterabgleich „ein Mechanismus zum Vergleichen eines Werts mit einem Muster. Ein gelungenes Match kann auch einen Wert in seine Bestandteile zerlegen.“

Dies darf nicht mit Regex, String-Matching oder Mustererkennung verwechselt werden. Pattern Matching hat nichts mit String zu tun, sondern mit Datenstruktur. Das erste Mal, dass ich auf Pattern Matching gestoßen bin, war vor etwa zwei Jahren, als ich Elixir ausprobierte. Ich habe Elixir gelernt und versucht, Algorithmen damit zu lösen. Ich verglich meine Lösung mit anderen und stellte fest, dass sie einen Musterabgleich verwendeten, wodurch ihr Code viel prägnanter und leichter lesbar wurde.

Aus diesem Grund hat mich der Musterabgleich wirklich beeindruckt. So sieht der Musterabgleich in Elixir aus:

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

Das obige Beispiel sieht sehr nach einer Mehrfachzuweisung in Ruby aus. Es ist jedoch mehr als das. Es prüft auch, ob die Werte übereinstimmen oder nicht:

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

In den obigen Beispielen ist die Zahl 42 auf der linken Seite keine Variable, die zugewiesen wird. Es ist ein Wert, um zu überprüfen, ob das gleiche Element in diesem bestimmten Index mit dem auf der rechten Seite übereinstimmt.

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

In diesem Beispiel wird anstelle der Zuweisung der Werte MatchError . Das liegt daran, dass die Zahl 88 nicht mit der Zahl 42 übereinstimmt.

Es funktioniert auch mit Karten (was Hash in Ruby ähnelt):

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

Das obige Beispiel prüft, Zote name , und bindet den Wert des title an den Variablentitel.

Dieses Konzept funktioniert sehr gut, wenn die Datenstruktur komplex ist. Sie können Ihre Variable zuweisen und in einer Zeile nach Werten oder Typen suchen.

Darüber hinaus erlaubt es einer dynamisch typisierten Sprache wie Elixir, Methoden zu überladen:

 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

Abhängig vom Schlüssel des Hashs des Arguments werden verschiedene Methoden ausgeführt.

Hoffentlich zeigt Ihnen das, wie leistungsfähig der Musterabgleich sein kann. Es gibt viele Versuche, den Musterabgleich mit Edelsteinen wie noaidi, qo und egison-ruby in Ruby zu bringen.

Ruby 2.7 hat auch seine eigene Implementierung, die sich nicht allzu sehr von diesen Edelsteinen unterscheidet, und so wird es derzeit gemacht.

Ruby Pattern Matching-Syntax

Der Musterabgleich in Ruby erfolgt über eine case -Anweisung. Anstatt jedoch das übliche when zu verwenden, wird stattdessen das Schlüsselwort in verwendet. Es unterstützt auch die Verwendung von if oder unless Anweisungen:

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

Die Case-Anweisung kann eine Variable oder einen Ausdruck akzeptieren, und dies wird mit Mustern abgeglichen, die in der in -Klausel angegeben sind. Wenn oder es sei denn Anweisungen können auch nach dem Muster bereitgestellt werden. Die Gleichheitsprüfung verwendet auch hier === wie die normale Case-Anweisung. Das bedeutet, dass Sie Teilmengen und Instanzen von Klassen abgleichen können. Hier ist ein Beispiel, wie Sie es verwenden:

Passende Arrays

 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

Im obigen Beispiel wird die variable translation mit zwei Mustern abgeglichen:

['th', orig_text, 'en', trans_text] und ['th', orig_text, 'ja', trans_text] . Es prüft, ob die Werte im Muster mit den Werten in der translation in jedem der Indizes übereinstimmen. Wenn die Werte übereinstimmen, weist es die Werte in der translation den Variablen im Muster in jedem der Indizes zu.

Ruby Pattern Matching-Animation: Matching-Arrays

Passende Hashes

 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

Im obigen Beispiel ist die translation jetzt ein Hash. Es wird mit einem anderen Hash in der in -Klausel abgeglichen. Was passiert ist, dass die case-Anweisung prüft, ob alle Schlüssel im Muster mit den Schlüsseln in der translation übereinstimmen. Es prüft auch, ob alle Werte für jeden Schlüssel übereinstimmen. Anschließend weist es die Werte der Variablen im Hash zu.

Ruby Pattern Matching-Animation: Matching-Arrays

Passende Teilmengen

Die beim Musterabgleich verwendete Qualitätsprüfung folgt der Logik von === .

Mehrere Muster

  • | kann verwendet werden, um mehrere Muster für einen Block zu definieren.
 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

Im obigen Beispiel wird die translation sowohl mit dem {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} als auch mit dem Hash ['th', orig_text, 'ja', trans_text] Reihe.

Dies ist nützlich, wenn Sie leicht unterschiedliche Arten von Datenstrukturen haben, die dasselbe darstellen, und Sie möchten, dass beide Datenstrukturen denselben Codeblock ausführen.

Pfeilzuweisung

In diesem Fall kann => verwendet werden, um einer Variablen einen übereinstimmenden Wert zuzuweisen.

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

Dies ist nützlich, wenn Sie Werte innerhalb der Datenstruktur überprüfen, diese Werte aber auch an eine Variable binden möchten.

Pin-Operator

Hier verhindert der Pin-Operator, dass Variablen neu zugewiesen werden.

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

Im obigen Beispiel wird die Variable a im Muster mit 1, 2 und dann 2 abgeglichen. Sie wird 1, dann 2, dann 2 zugewiesen. Dies ist keine ideale Situation, wenn Sie das alles überprüfen möchten Werte im Array sind gleich.

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

Wenn der Pin-Operator verwendet wird, wertet er die Variable aus, anstatt sie neu zuzuweisen. Im obigen Beispiel stimmt [1,2,2] nicht mit [a,^a,^a] überein, da a im ersten Index 1 zugewiesen ist. Im zweiten und dritten wird a als 1 ausgewertet. wird aber gegen 2 gematcht.

[a,b,^b] stimmt jedoch mit [1,2,2] überein, da a im ersten Index 1 zugewiesen ist, b im zweiten Index 2 zugewiesen ist und dann ^b, das jetzt 2 ist, abgeglichen wird 2 im dritten Index, also geht es weiter.

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

Es können auch Variablen außerhalb der case-Anweisung verwendet werden, wie im obigen Beispiel gezeigt.

Unterstrich ( _ ) Operator

Der Unterstrich ( _ ) wird verwendet, um Werte zu ignorieren. Sehen wir es uns an ein paar Beispielen an:

 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

In den beiden obigen Beispielen wird jeder Wert, der mit _ übereinstimmt, bestanden. In der zweiten case-Anweisung erfasst der => -Operator auch den Wert, der ignoriert wurde.

Anwendungsfälle für den Musterabgleich in Ruby

Stellen Sie sich vor, Sie haben die folgenden JSON-Daten:

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

In Ihrem Ruby-Projekt möchten Sie diese Daten parsen und den Namen mit den folgenden Bedingungen anzeigen:

  1. Wenn der Benutzername vorhanden ist, geben Sie den Benutzernamen zurück.
  2. Wenn der Spitzname, der Vorname und der Nachname vorhanden sind, geben Sie den Spitznamen, den Vornamen und dann den Nachnamen zurück.
  3. Wenn der Spitzname nicht existiert, aber der Vor- und Nachname existieren, geben Sie den Vornamen und dann den Nachnamen zurück.
  4. Wenn keine der Bedingungen zutrifft, geben Sie „Neuer Benutzer“ zurück.

So würde ich dieses Programm jetzt in Ruby schreiben:

 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

Sehen wir uns nun an, wie es mit dem Musterabgleich aussieht:

 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

Syntaxpräferenzen können ein wenig subjektiv sein, aber ich bevorzuge die Pattern-Matching-Version. Dies liegt daran, dass der Musterabgleich es uns ermöglicht, den erwarteten Hash zu schreiben, anstatt die Werte des Hashs zu beschreiben und zu überprüfen. Dies erleichtert die Visualisierung der zu erwartenden Daten:

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

Anstatt:

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

Deconstruct und Deconstruct_keys

In Ruby 2.7 werden zwei neue spezielle Methoden eingeführt: deconstruct und deconstruct_keys . Wenn eine Instanz einer Klasse mit einem Array oder Hash abgeglichen wird, werden deconstruct bzw. deconstruct_keys aufgerufen.

Die Ergebnisse dieser Methoden werden zum Abgleich mit Mustern verwendet. Hier ist ein Beispiel:

 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

Der Code definiert eine Klasse namens Coordinate . Es hat x und y als seine Attribute. Außerdem sind die Methoden deconstruct und deconstruct_keys definiert.

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

Hier wird eine Instanz von Coordinate definiert und ein Muster mit einem Array abgeglichen. Was hier passiert, ist, dass Coordinate#deconstruct aufgerufen wird und das Ergebnis verwendet wird, um es mit dem im Muster definierten Array [a,b] abzugleichen.

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

In diesem Beispiel wird dieselbe Instanz von Coordinate mit einem Hash verglichen. In diesem Fall wird das Coordinate#deconstruct_keys -Ergebnis verwendet, um es mit dem im Muster definierten Hash {x: x, y: y} abzugleichen.

Eine aufregende experimentelle Funktion

Nachdem ich zum ersten Mal Mustervergleiche in Elixir erlebt hatte, dachte ich, diese Funktion könnte das Überladen von Methoden beinhalten und mit einer Syntax implementiert werden, die nur eine Zeile erfordert. Ruby ist jedoch keine Sprache, die auf Mustererkennung ausgelegt ist, daher ist dies verständlich.

Die Verwendung einer case-Anweisung ist wahrscheinlich eine sehr schlanke Art, dies zu implementieren, und wirkt sich auch nicht auf bestehenden Code aus (abgesehen von den Methoden deconstruct und deconstruct_keys ). Die Verwendung der case-Anweisung ähnelt tatsächlich der von Scalas Implementierung des Mustervergleichs.

Ich persönlich denke, dass der Musterabgleich ein aufregendes neues Feature für Ruby-Entwickler ist. Es hat das Potenzial, den Code viel sauberer zu machen und Ruby ein bisschen moderner und aufregender zu machen. Ich würde gerne sehen, was die Leute daraus machen und wie sich diese Funktion in Zukunft entwickelt.