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 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