Pattern Matching
Pattern Matching ist eine Funktion, die eine tiefgehende Abgleichung von strukturierten Werten ermöglicht: Überprüfung der Struktur und Bindung der abgeglichenen Teile an lokale Variablen.
Pattern Matching in Ruby wird mit dem case/in Ausdruck implementiert
case <expression> in <pattern1> ... in <pattern2> ... in <pattern3> ... else ... end
(Beachten Sie, dass in und when Zweige NICHT in einem einzigen case Ausdruck gemischt werden können.)
Oder mit dem => Operator und dem in Operator, der als eigenständiger Ausdruck verwendet werden kann
<expression> => <pattern> <expression> in <pattern>
Der case/in Ausdruck ist erschöpfend: Wenn der Wert des Ausdrucks keinem Zweig des case Ausdrucks entspricht (und der else Zweig fehlt), wird NoMatchingPatternError ausgelöst.
Daher kann der case Ausdruck für bedingte Abgleiche und Entpackungen verwendet werden
config = {db: {user: 'admin', password: 'abc123'}} case config in db: {user:} # matches subhash and puts matched value in variable user puts "Connect with user '#{user}'" in connection: {username: } puts "Connect with user '#{username}'" else puts "Unrecognized structure of config" end # Prints: "Connect with user 'admin'"
während der => Operator am nützlichsten ist, wenn die erwartete Datenstruktur im Voraus bekannt ist, um einfach Teile davon zu entpacken
config = {db: {user: 'admin', password: 'abc123'}} config => {db: {user:}} # will raise if the config's structure is unexpected puts "Connect with user '#{user}'" # Prints: "Connect with user 'admin'"
<expression> in <pattern> ist dasselbe wie case <expression>; in <pattern>; true; else false; end. Sie können es verwenden, wenn Sie nur wissen möchten, ob ein Muster abgeglichen wurde oder nicht
users = [{name: "Alice", age: 12}, {name: "Bob", age: 23}] users.any? {|user| user in {name: /B/, age: 20..} } #=> true
Siehe unten für weitere Beispiele und Erklärungen der Syntax.
Muster
Muster können sein
-
jedes Ruby-Objekt (abgeglichen mit dem
===Operator, wie inwhen); (Wertmuster) -
Array-Muster:
[<subpattern>, <subpattern>, <subpattern>, ...]; (Array-Muster) -
Find-Muster:
[*variable, <subpattern>, <subpattern>, <subpattern>, ..., *variable]; (Find-Muster) -
Hash-Muster:
{key: <subpattern>, key: <subpattern>, ...}; (Hash-Muster) -
Kombination von Mustern mit
|; (Alternativmuster) -
Variablenbindung:
<pattern> => variableodervariable; (As-Muster, Variablenmuster)
Jedes Muster kann in Array-/Find-/Hash-Mustern verschachtelt werden, wo <subpattern> angegeben ist.
Array Muster und Find-Muster gleichen Arrays ab, oder Objekte, die auf deconstruct reagieren (siehe unten dazu). Hash Muster gleichen Hashes ab, oder Objekte, die auf deconstruct_keys reagieren (siehe unten dazu). Beachten Sie, dass für Hash-Muster nur Symbolschlüssel unterstützt werden.
Ein wichtiger Unterschied im Verhalten von Array- und Hash-Mustern ist, dass Arrays nur ein ganzes Array abgleichen
case [1, 2, 3] in [Integer, Integer] "matched" else "not matched" end #=> "not matched"
während der Hash auch dann abgleicht, wenn andere Schlüssel neben dem angegebenen Teil vorhanden sind
case {a: 1, b: 2, c: 3} in {a: Integer} "matched" else "not matched" end #=> "matched"
{} ist die einzige Ausnahme von dieser Regel. Es gleicht nur ab, wenn ein leerer Hash übergeben wird
case {a: 1, b: 2, c: 3} in {} "matched" else "not matched" end #=> "not matched" case {} in {} "matched" else "not matched" end #=> "matched"
Es gibt auch eine Möglichkeit anzugeben, dass im abgeglichenen Hash keine anderen Schlüssel vorhanden sein sollen als die, die explizit durch das Muster angegeben sind, mit **nil
case {a: 1, b: 2} in {a: Integer, **nil} # this will not match the pattern having keys other than a: "matched a part" in {a: Integer, b: Integer, **nil} "matched a whole" else "not matched" end #=> "matched a whole"
Sowohl Array- als auch Hash-Muster unterstützen die „Rest“-Spezifikation
case [1, 2, 3] in [Integer, *] "matched" else "not matched" end #=> "matched" case {a: 1, b: 2, c: 3} in {a: Integer, **} "matched" else "not matched" end #=> "matched"
Klammern um beide Arten von Mustern konnten weggelassen werden
case [1, 2] in Integer, Integer "matched" else "not matched" end #=> "matched" case {a: 1, b: 2, c: 3} in a: Integer "matched" else "not matched" end #=> "matched" [1, 2] => a, b [1, 2] in a, b {a: 1, b: 2, c: 3} => a: {a: 1, b: 2, c: 3} in a:
Das Find-Muster ist dem Array-Muster ähnlich, kann aber verwendet werden, um zu prüfen, ob das gegebene Objekt Elemente hat, die dem Muster entsprechen
case ["a", 1, "b", "c", 2] in [*, String, String, *] "matched" else "not matched" end
Variablenbindung
Neben tiefen Strukturprüfungen ist eine der sehr wichtigen Funktionen des Pattern Matching die Bindung der abgeglichenen Teile an lokale Variablen. Die Grundform der Bindung besteht darin, einfach => variable_name nach dem abgeglichenen (Teil-)Muster anzugeben (man mag dies ähnlich finden wie das Speichern von Exceptions in lokalen Variablen in einer rescue ExceptionClass => var Klausel)
case [1, 2] in Integer => a, Integer "matched: #{a}" else "not matched" end #=> "matched: 1" case {a: 1, b: 2, c: 3} in a: Integer => m "matched: #{m}" else "not matched" end #=> "matched: 1"
Wenn keine zusätzliche Prüfung erforderlich ist, um nur einen Teil der Daten an eine Variable zu binden, kann eine einfachere Form verwendet werden
case [1, 2] in a, Integer "matched: #{a}" else "not matched" end #=> "matched: 1" case {a: 1, b: 2, c: 3} in a: m "matched: #{m}" else "not matched" end #=> "matched: 1"
Für Hash-Muster gibt es sogar eine noch einfachere Form: schlüsselbasierte Spezifikation (ohne Unter-Muster) bindet die lokale Variable auch mit dem Namen des Schlüssels
case {a: 1, b: 2, c: 3} in a: "matched: #{a}" else "not matched" end #=> "matched: 1"
Bindung funktioniert auch für verschachtelte Muster
case {name: 'John', friends: [{name: 'Jane'}, {name: 'Rajesh'}]} in name:, friends: [{name: first_friend}, *] "matched: #{first_friend}" else "not matched" end #=> "matched: Jane"
Der „Rest“-Teil eines Musters kann ebenfalls an eine Variable gebunden werden
case [1, 2, 3] in a, *rest "matched: #{a}, #{rest}" else "not matched" end #=> "matched: 1, [2, 3]" case {a: 1, b: 2, c: 3} in a:, **rest "matched: #{a}, #{rest}" else "not matched" end #=> "matched: 1, {b: 2, c: 3}"
Bindung an Variablen funktioniert derzeit NICHT für Alternativmuster, die mit | verbunden sind
case {a: 1, b: 2}
in {a: } | Array
# ^ SyntaxError (variable capture in alternative pattern)
"matched: #{a}"
else
"not matched"
end
Variablen, die mit _ beginnen, sind die einzigen Ausnahmen von dieser Regel
case {a: 1, b: 2} in {a: _, b: _foo} | Array "matched: #{_}, #{_foo}" else "not matched" end # => "matched: 1, 2"
Es ist jedoch nicht ratsam, den gebundenen Wert wiederzuverwenden, da das Ziel dieses Musters die Kennzeichnung eines verworfenen Werts ist.
Variablen-Pinning
Aufgrund der Variablenbindungsfunktion können bestehende lokale Variablen nicht ohne weiteres als Unter-Muster verwendet werden
expectation = 18 case [1, 2] in expectation, *rest "matched. expectation was: #{expectation}" else "not matched. expectation was: #{expectation}" end # expected: "not matched. expectation was: 18" # real: "matched. expectation was: 1" -- local variable just rewritten
In diesem Fall kann der Pin-Operator ^ verwendet werden, um Ruby mitzuteilen „verwende diesen Wert einfach als Teil des Musters“
expectation = 18 case [1, 2] in ^expectation, *rest "matched. expectation was: #{expectation}" else "not matched. expectation was: #{expectation}" end #=> "not matched. expectation was: 18"
Eine wichtige Anwendung des Variablen-Pinning ist die Angabe, dass derselbe Wert mehrmals im Muster vorkommen soll
jane = {school: 'high', schools: [{id: 1, level: 'middle'}, {id: 2, level: 'high'}]} john = {school: 'high', schools: [{id: 1, level: 'middle'}]} case jane in school:, schools: [*, {id:, level: ^school}] # select the last school, level should match "matched. school: #{id}" else "not matched" end #=> "matched. school: 2" case john # the specified school level is "high", but last school does not match in school:, schools: [*, {id:, level: ^school}] "matched. school: #{id}" else "not matched" end #=> "not matched"
Neben dem Pinning von lokalen Variablen können auch Instanz-, globale und Klassenvariablen gepinnt werden
$gvar = 1 class A @ivar = 2 @@cvar = 3 case [1, 2, 3] in ^$gvar, ^@ivar, ^@@cvar "matched" else "not matched" end #=> "matched" end
Sie können auch das Ergebnis beliebiger Ausdrücke mithilfe von Klammern pinnen
a = 1 b = 2 case 3 in ^(a + b) "matched" else "not matched" end #=> "matched"
Abgleich von Nicht-Primitive-Objekten: deconstruct und deconstruct_keys
Wie bereits oben erwähnt, versuchen Array-, Find- und Hash-Muster, zusätzlich zu literalen Arrays und Hashes, jedes Objekt abzugleichen, das deconstruct (für Array/Find-Muster) oder deconstruct_keys (für Hash-Muster) implementiert.
class Point def initialize(x, y) @x, @y = x, y end def deconstruct puts "deconstruct called" [@x, @y] end def deconstruct_keys(keys) puts "deconstruct_keys called with #{keys.inspect}" {x: @x, y: @y} end end case Point.new(1, -2) in px, Integer # sub-patterns and variable binding works "matched: #{px}" else "not matched" end # prints "deconstruct called" "matched: 1" case Point.new(1, -2) in x: 0.. => px "matched: #{px}" else "not matched" end # prints: deconstruct_keys called with [:x] #=> "matched: 1"
keys werden an deconstruct_keys übergeben, um Optimierungsspielraum in der abgeglichenen Klasse zu bieten: Wenn die Berechnung einer vollständigen Hash-Repräsentation teuer ist, kann nur der notwendige Subhash berechnet werden. Wenn das **rest-Muster verwendet wird, wird nil als Wert für keys übergeben
case Point.new(1, -2) in x: 0.. => px, **rest "matched: #{px}" else "not matched" end # prints: deconstruct_keys called with nil #=> "matched: 1"
Zusätzlich kann beim Abgleichen von benutzerdefinierten Klassen die erwartete Klasse als Teil des Musters angegeben werden und wird mit === geprüft
class SuperPoint < Point end case Point.new(1, -2) in SuperPoint(x: 0.. => px) "matched: #{px}" else "not matched" end #=> "not matched" case SuperPoint.new(1, -2) in SuperPoint[x: 0.. => px] # [] or () parentheses are allowed "matched: #{px}" else "not matched" end #=> "matched: 1"
Diese Kern- und Bibliotheksklassen implementieren die Dekonstruktion
Guard-Klauseln
if kann verwendet werden, um eine zusätzliche Bedingung (Guard-Klausel) anzuhängen, wenn das Muster in case/in Ausdrücken übereinstimmt. Diese Bedingung kann gebundene Variablen verwenden
case [1, 2] in a, b if b == a*2 "matched" else "not matched" end #=> "matched" case [1, 1] in a, b if b == a*2 "matched" else "not matched" end #=> "not matched"
unless funktioniert ebenfalls
case [1, 1] in a, b unless b == a*2 "matched" else "not matched" end #=> "matched"
Beachten Sie, dass => und der in Operator keine Guard-Klausel haben können. Die folgenden Beispiele werden als eigenständiger Ausdruck mit dem Modifikator if geparst.
[1, 2] in a, b if b == a*2
Anhang A. Syntax von Mustern
Ungefähre Syntax ist
pattern: value_pattern
| variable_pattern
| alternative_pattern
| as_pattern
| array_pattern
| find_pattern
| hash_pattern
value_pattern: literal
| Constant
| ^local_variable
| ^instance_variable
| ^class_variable
| ^global_variable
| ^(expression)
variable_pattern: variable
alternative_pattern: pattern | pattern | ...
as_pattern: pattern => variable
array_pattern: [pattern, ..., *variable]
| Constant(pattern, ..., *variable)
| Constant[pattern, ..., *variable]
find_pattern: [*variable, pattern, ..., *variable]
| Constant(*variable, pattern, ..., *variable)
| Constant[*variable, pattern, ..., *variable]
hash_pattern: {key: pattern, key:, ..., **variable}
| Constant(key: pattern, key:, ..., **variable)
| Constant[key: pattern, key:, ..., **variable]
Anhang B. Einige Beispiele für undefiniertes Verhalten
Um zukünftige Optimierungen zu ermöglichen, enthält die Spezifikation einige undefinierte Verhaltensweisen.
Verwendung einer Variablen in einem nicht übereinstimmenden Muster
case [0, 1] in [a, 2] "not matched" in b "matched" in c "not matched" end a #=> undefined c #=> undefined
Anzahl der Aufrufe der Methoden deconstruct, deconstruct_keys
$i = 0 ary = [0] def ary.deconstruct $i += 1 self end case ary in [0, 1] "not matched" in [0] "matched" end $i #=> undefined