Kapitel 4. Musterabgleich

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Das Pattern-Matching von Scala ermöglicht eine tiefe Untersuchung und Zerlegung von Objekten auf verschiedene Arten. Das ist eine meiner Lieblingsfunktionen in Scala. Für deine eigenen Typen kannst du einem Protokoll folgen, mit dem du die Sichtbarkeit des internen Zustands und die Art und Weise, wie er den Nutzern zugänglich gemacht werden soll, steuern kannst. Die Begriffe Extraktion und Destrukturierung werden manchmal für diese Fähigkeit verwendet.

Wie wir bereits in "Eine Beispielanwendung" und "Teilfunktionen" gesehen haben, kann Pattern Matching in verschiedenen Code-Kontexten verwendet werden . Wir beginnen mit einer Änderung in Scala 3 zur Verbesserung der Typsicherheit, gefolgt von einem kurzen Überblick über gängige und einfache Anwendungsbeispiele, bevor wir fortgeschrittenere Szenarien untersuchen. In späteren Kapiteln werden wir uns mit einigen weiteren Pattern-Matching-Funktionen befassen, sobald wir den nötigen Hintergrund haben, um sie zu verstehen.

Sicheres Pattern Matching mit Matchable

Beginnen wir mit einer wichtigen Änderung im Typsystem von Scala 3, die die Kompilierzeitüberprüfung von musterübereinstimmenden Ausdrücken robuster machen soll.

Mit Scala 3 wurde ein unveränderlicher Wrapper um Arrays eingeführt. scala.IArray. Arrayist in Java veränderbar, daher ist dies ein sicherer Weg, um mit ihnen zu arbeiten. Tatsächlich ist IArray ein Typ-Alias für Array, um den Overhead des Wrappings von Arrays zu vermeiden, was bedeutet, dass der Musterabgleich ein Loch in die Abstraktion reißt. Wenn du die Scala 3.0 REPL ohne die Einstellung -source:future verwendest, kannst du Folgendes beobachten:

// src/script/scala/progscala3/patternmatching/Matchable.scala
scala> val iarray = IArray(1,2,3,4,5)
     | iarray match
     |   case a: Array[Int] => a(2) = 300 // Scala 3 warning!!
     | println(iarray)
val iarray: opaques.IArray[Int] = Array(1, 2, 300, 4, 5)

Es gibt noch weitere Beispiele, bei denen dies vorkommen kann. Um dieses Schlupfloch zu schließen, hat das Scala-Typsystem jetzt eine Eigenschaft namens Matchable. Er fügt sich wie folgt in die Typenhierarchie ein:

abstract class Any:
  def isInstanceOf
  def getClass
  def asInstanceOf      // Cast to a new type: myAny.asInstanceOf[String]
  def ==
  def !=
  def ##   // Alias for hashCode
  def equals
  def hashCode
  def toString

trait Matchable extends Any

class AnyVal extends Any, Matchable

class AnyRef extends Any, Matchable

Beachte, dass Matchable eine Marker-Eigenschaft ist, da sie derzeit keine Mitglieder hat. In einer zukünftigen Version von Scala könnten getClass und isInstanceOf jedoch in Matchable verschoben werden, da sie eng mit dem Mustervergleich verbunden sind.

Die Absicht ist, dass der Mustervergleich nur auf Werte des Typs Matchable und nicht auf Any angewendet werden kann. Da fast alle Typen Untertypen von AnyRef und AnyVal sind, erfüllen sie bereits diese Einschränkung. Der Versuch, Mustervergleiche für die folgenden Typen durchzuführen, wird in zukünftigen Scala 3 Versionen oder bei der Verwendung von -source:future mit Scala 3.0 Warnungen auslösen:

  • Gib Any ein. Verwende stattdessen Matchable, wenn möglich.

  • Typparameter und abstrakte Typen ohne Begrenzungen. Füge <: Matchable hinzu.

  • Typparameter und abstrakte Typen, die nur durch universelle Eigenschaften begrenzt sind. Füge <: Matchable hinzu.

Wir werden die universellen Eigenschaften in "Werteklassen" besprechen . Für den Moment können wir sie ignorieren. Als Beispiel für den zweiten Punkt betrachten wir die folgende Methodendefinition in einer REPL-Sitzung mit dem wiederhergestellten -source:future Flag:

scala> def examine[T](seq: Seq[T]): Seq[String] = seq map {
     |   case i: Int => s"Int: $i"
     |   case other => s"Other: $other"
     | }
2 |  case i: Int => s"Int: $i"
  |          ^^^
  |          pattern selector should be an instance of Matchable,
  |          but it has unmatchable type T instead

Jetzt braucht der Typ-Parameter T eine Begrenzung:

scala> def examine[T <: Matchable](seq: Seq[T]): Seq[String] = seq map {
     |   case i: Int => s"Int: $i"
     |   case other => s"Other: $other"
     | }
def examine[T <: Matchable](seq: Seq[T]): Seq[String]

scala> val seq = Seq(1, "two", 3, 4.4)
     | examine(seq)
val seq: Seq[Matchable] = List(1, two, 3, 4.4)
val res0: Seq[String] = List(Int: 1, Other: two, Int: 3, Other: 4.4)

Beachte den gefolgerten gemeinsamen Supertyp der Werte in der Sequenz, seq. In Scala 2 wäre dies Any.

Zurück zu IArray, das Beispiel am Anfang löst jetzt eine Warnung aus, weil der Alias IArray nicht von Matchable begrenzt wird:

scala> val iarray = IArray(1,2,3,4,5)
     | iarray match
     |   case a: Array[Int] => a(2) = 300
     |
3 |  case a: Array[Int] => a(2) = 300
  |          ^^^^^^^^^^
  |          pattern selector should be an instance of Matchable,
  |          but it has unmatchable type opaques.IArray[Int] instead

IArray wird vom Compiler als abstrakter Typ betrachtet. Abstrakte Typen sind nicht durch Matchable begrenzt, weshalb wir jetzt die gewünschte Warnung erhalten.

Dies ist eine bedeutende Änderung, die eine Menge bestehenden Code zerstören wird. Daher werden Warnungen erst ab einem zukünftigen Scala 3 Release oder beim Kompilieren mit -source:future.

Werte, Variablen und Typen in Übereinstimmungen

Gehen wir auf verschiedene Arten von Übereinstimmungen ein. Das folgende Beispiel zeigt Übereinstimmungen mit bestimmten Werten, allen Werten eines bestimmten Typs und eine Möglichkeit, eine Standardklausel zu schreiben, die auf alles passt:

// src/script/scala/progscala3/patternmatching/MatchVariable.scala

val seq = Seq(1, 2, 3.14, 5.5F, "one", "four", true, (6, 7))    1
val result = seq.map {
  case 1                   => "int 1"                           2
  case i: Int              => s"other int: $i"
  case d: (Double | Float) => s"a double or float: $d"          3
  case "one"               => "string one"                      4
  case s: String           => s"other string: $s"
  case (x, y)              => s"tuple: ($x, $y)"                5
  case unexpected          => s"unexpected value: $unexpected"  6
}
assert(result == Seq(
  "int 1", "other int: 2",
  "a double or float: 3.14", "a double or float: 5.5",
  "string one", "other string: four",
  "unexpected value: true",
  "tuple: (6, 7)"))
1

Wegen der Mischung der Werte ist seq vom Typ Seq[Matchable].

2

Wenn eine oder mehrere Fallklauseln bestimmte Werte eines Typs spezifizieren, müssen sie vor allgemeineren Klauseln stehen, die nur auf den Typ passen. Wir prüfen also zuerst, ob der anonyme Wert Int gleich 1 ist. Wenn ja, geben wir einfach die Zeichenfolge "int 1" zurück. Wenn der Wert ein anderer Int Wert ist, passt die nächste Klausel. In diesem Fall wird der Wert in Int umgewandelt und der Variablen i zugewiesen, die zum Aufbau einer Zeichenkette verwendet wird.

3

Übereinstimmung mit einem beliebigen Double oder Float Wert. Die Verwendung von | ist praktisch, wenn zwei oder mehr Fälle auf die gleiche Weise behandelt werden. Damit dies funktioniert, muss die Logik nach => jedoch für alle übereinstimmenden Typen kompatibel sein. In diesem Fall funktioniert die interpolierte Zeichenkette gut.

4

Zwei Fallklauseln für Strings.

5

Finde ein Tupel mit zwei Elementen, wobei die Elemente von beliebigem Typ sind, und extrahiere die Elemente in die Variablen x und y.

6

Alle anderen Eingaben passen. Die Variable unexpected hat einen beliebigen Namen. Da keine Typdeklaration angegeben ist, wird Matchable abgeleitet. Dies fungiert als Standardklausel. Der boolesche Wert aus der Sequenz seq wird unexpected zugewiesen.

Wir haben eine Teilfunktion an Seq.map() übergeben. Erinnere dich daran, dass die Literal-Syntax case Anweisungen erfordert, und wir haben die Teilfunktion in Klammern gesetzt, um sie an map zu übergeben. Diese Funktion ist jedoch tatsächlich total, weil die letzte Klausel auf jeden Matchable passt (in Scala 2 wäre es Any ). Das bedeutet, dass sie nicht auf Instanzen der wenigen anderen Typen passt, die nicht Matchablesind, wie IArray, aber diese Typen sind keine Kandidaten mehr für den Mustervergleich. Von nun an werde ich Teilfunktionen wie diese einfach total nennen.

Verwende keine Klauseln mit bestimmten Fließkomma-Literalen, denn die Übereinstimmung mit Fließkomma-Literalen ist eine schlechte Idee. Rundungsfehler können dazu führen, dass zwei Werte, von denen man annehmen könnte, dass sie gleich sind, in Wirklichkeit unterschiedlich sind.

Übereinstimmungen sind eifrig, also müssen spezifischere Klauseln vor weniger spezifischen Klauseln erscheinen. Andernfalls bekommen die spezifischeren Klauseln nie die Chance, zu passen. Daher müssen die Klauseln, die auf bestimmte Werte von Typen passen, vor den Klauseln stehen, die auf den Typ passen (d. h. auf jeden Wert des Typs). Die angezeigte Standardklausel muss die letzte sein. Glücklicherweise gibt der Compiler eine "Unreachable case"-Warnung aus, wenn du diesen Fehler machst. Tausche die beiden Int Klauseln aus, um zu sehen, was passiert.

Match Klauseln sind Ausdrücke, also geben sie einen Wert zurück. In diesem Beispiel geben alle Klauseln Zeichenketten zurück, so dass der Rückgabetyp des Ausdrucks match (und der Teilfunktion) String ist. Der Rückgabetyp des Aufrufs map ist daher List[String]. Der Compiler ermittelt die kleinste obere Grenze, den nächstgelegenen Supertyp, für die Typen der Werte, die von allen case Klauseln zurückgegeben werden.

Das ist natürlich ein erfundenes Beispiel. Wenn du Ausdrücke entwickelst, die mit Mustern übereinstimmen, sei vorsichtig, wenn du dich auf eine Standardklausel case verlässt. Unter welchen Umständen wäre "keine der oben genannten" die richtige Antwort? Es könnte darauf hindeuten, dass dein Entwurf verfeinert werden könnte, damit du alle möglichen Übereinstimmungen genauer kennst, z. B. eine versiegelte Typenhierarchie oder enum, auf die wir noch eingehen werden. Im Laufe dieses Kapitels wirst du mehr realistische Szenarien und keine Standardklauseln sehen.

Hier ist ein ähnliches Beispiel, bei dem eine anonyme Funktion an map übergeben wird, anstatt einer Teilfunktion, sowie einige andere Änderungen:

// src/script/scala/progscala3/patternmatching/MatchVariable2.scala

val seq2 = Seq(1, 2, 3.14, "one", (6, 7))
val result2 = seq2.map { x => x match
  case _: Int  => s"int: $x"                                    1
  case _       => s"unexpected value: $x"                       2
}
assert(result2 == Seq(
  "int: 1", "int: 2", "unexpected value: 3.14",
  "unexpected value: one", "unexpected value: (6,7)"))
1

Verwende _ für den Variablennamen, das heißt, wir erfassen ihn nicht.

2

Catch-All-Klausel, die auch x verwendet, anstatt eine neue Variable zu erfassen.

Die erste Fallklausel muss die Variable nicht erfassen, weil sie die Tatsache, dass der Wert ein Int ist, nicht ausnutzt. Sie ruft zum Beispiel keine Int Methoden auf. Andernfalls würde die Verwendung von x nicht ausreichen, da sie den Typ Matchable hat.

Auch hier werden geschweifte Klammern um die gesamte anonyme Funktion herum verwendet, aber die optionale Klammersyntax wird innerhalb der Funktion für den Ausdruck match verwendet. Im Allgemeinen ist die Verwendung einer Teilfunktion prägnanter, da wir die x => x match nicht benötigen.

Tipp

Wenn du den Mustervergleich mit einer der Sammelmethoden wie map und foreach verwendest, benutze eine Teilfunktion.

Für case Klauseln gibt es ein paar Regeln und Probleme zu beachten. Der Compiler geht davon aus, dass ein Begriff, der mit einem Kleinbuchstaben beginnt, der Name einer Variablen ist, die einen übereinstimmenden Wert enthalten wird. Wenn der Begriff mit einem Großbuchstaben beginnt, erwartet er eine Definition, die sich bereits im Gültigkeitsbereich befindet.

Diese Kleinbuchstabenregel kann zu Überraschungen führen, wie das folgende Beispiel zeigt. Die Absicht ist, einen Wert an eine Methode zu übergeben und dann zu sehen, ob dieser Wert mit einem Element in der Sammlung übereinstimmt:

// src/script/scala/progscala3/patternmatching/MatchSurprise.scala

def checkYBad(y: Int): Seq[String] =
  for x <- Seq(99, 100, 101)
  yield x match
    case y => "found y!"
    case i: Int => "int: "+i  // Unreachable case!

Die erste Fallklausel soll auf den Wert passen, der als y übergeben wird, aber das ist, was wir tatsächlich erhalten:

def checkBad(y: Int): Seq[String]
10 |      case i: Int => "int: "+i  // Unreachable case!
   |           ^^^^^^
   |           Unreachable case

In unseren built.sbt Einstellungen behandeln wir Warnungen als Fehler, aber wenn wir das nicht täten, würde der Aufruf von checkY(100) für alle drei Zahlen found y! zurückgeben.

Die Klausel case y bedeutet: "Finde irgendetwas, weil es keine Typdeklaration gibt, und weise es dieser neuen Variablen namens y zu." Das y in der Klausel wird nicht als Verweis auf den Methodenparameter y interpretiert. Vielmehr stellt es diese Definition in den Schatten. Daher ist diese Klausel eigentlich eine Standardklausel, die alles abgleicht, und wir werden nie die zweite case Klausel erreichen.

Es gibt zwei Lösungen. Erstens könnten wir Y verwenden, obwohl es seltsam aussieht, wenn ein Methodenparameter mit einem Großbuchstaben beginnt:

def checkYGood1(Y: Int): Seq[String] =
  for x <- Seq(99, 100, 101)
  yield x match
    case Y => "found y!"
    case i: Int => "int: "+i

Der Aufruf von checkYGood1(100) gibt List(int: 99, found y!, int: 101) zurück.

Die zweite Lösung ist die Verwendung von Backticks, um anzuzeigen, dass wir wirklich mit dem Wert von y übereinstimmen wollen:

def checkYGood2(y: Int): Seq[String] =
  for x <- Seq(99, 100, 101)
  yield x match
    case `y` => "found y!"
    case i: Int => "int: "+i
Warnung

In case Klauseln wird davon ausgegangen, dass ein Begriff, der mit einem Kleinbuchstaben beginnt, der Name einer neuen Variablen ist, die einen extrahierten Wert enthalten wird. Um auf eine bereits definierte Variable zu verweisen, musst du sie in Backticks einschließen oder den Namen mit einem Großbuchstaben beginnen.

Schließlich sollten die meisten match Ausdrücke erschöpfend sein:

// src/script/scala/progscala3/patternmatching/MatchExhaustive.scala

scala> val seq3 = Seq(Some(1), None, Some(2), None)
val seq3: Seq[Option[Int]] = List(Some(1), None, Some(2), None)

scala> val result3 = seq3.map {
     |   case Some(i)  => s"int $i"
     | }
5 |  case Some(i)  => s"int $i"
  |  ^
  |  match may not be exhaustive.
  |
  |  It would fail on pattern case: None

Der Compiler weiß, dass die Elemente von seq3 vom Typ Option[Int] sind, der auch Elemente von None enthalten kann. Zur Laufzeit wird ein MatchError ausgelöst, wenn ein None angetroffen wird. Die Lösung ist ganz einfach:

// src/script/scala/progscala3/patternmatching/MatchExhaustiveFix.scala

scala> val result3 = seq3.map {
     |   case Some(i)  => s"int $i"
     |   case None     => ""
     | }
val result3: Seq[String] = List(int 1, "", int 2, "")

Im Abschnitt "Probleme bei Musterbindungen" werden weitere Punkte zum erschöpfenden Abgleich besprochen.

Abgleich auf Sequenzen

Untersuchen wir das klassische Idiom für die Iteration durch eine Seq mit Mustervergleich und Rekursion und lernen wir dabei einige nützliche Grundlagen über Sequenzen:

// src/script/scala/progscala3/patternmatching/MatchSeq.scala

def seqToString[T](seq: Seq[T]): String = seq match                  1
  case head +: tail => s"($head +: ${seqToString(tail)})"            2
  case Nil => "Nil"                                                  3
1

Definiere eine rekursive Methode, die eine String aus einer Seq[T] für einen Typ T konstruiert, der aus der übergebenen Sequenz gefolgert wird. Der Körper ist ein einzelner match Ausdruck.

2

Es gibt zwei Übereinstimmungsklauseln und sie sind erschöpfend. Die erste passt auf jedes nicht leere Seq, wobei das erste Element als head und der Rest von Seq als tail extrahiert wird. Dies sind gängige Bezeichnungen für die Teile eines Seq, das über die Methoden head und tail verfügt. Hier werden diese Begriffe jedoch als Variablennamen verwendet. Der Körper der Klausel konstruiert eine String mit dem Kopf, gefolgt von +: und dem Ergebnis des Aufrufs von seqToString am Ende, alles umgeben von Klammern, (). Beachte, dass diese Methode rekursiv ist, aber nicht am Ende.

3

Der einzige andere mögliche Fall ist ein leeres Seq. Wir können das Spezialfall-Objekt für einen leeren List verwenden, Nilverwenden, um alle leeren Fälle zu finden. Mit dieser Klausel wird die Rekursion beendet. Beachte, dass jeder Typ von Seq immer so interpretiert werden kann, dass er mit Nil endet, oder wir können eine leere Instanz des aktuellen Typs verwenden (Beispiele folgen).

Der Operator +: ist der cons (Konstruktions)-Operator für Sequenzen. Erinnere dich daran, dass Methoden, die mit einem Doppelpunkt enden (:), nach rechts gebunden werden, in Richtung des Seq Endes. Allerdings ist +: in dieser case Klausel eigentlich ein object namens +:So haben wir eine schöne Syntax-Symmetrie zwischen der Konstruktion von Sequenzen, wie val seq = 1 +: 2 +: Nil, und der Dekonstruktion, wie case 1 +: 2 +: Nil =>.... Später in diesem Kapitel werden wir sehen, wie eine object verwendet wird, um die Dekonstruktion zu implementieren.

Diese beiden Klauseln schließen sich gegenseitig aus, sodass sie mit der Nil Klausel zuerst geschrieben werden können.

Jetzt wollen wir es mit verschiedenen leeren und nicht leeren Sequenzen ausprobieren:

scala> seqToString(Seq(1, 2, 3))
     | seqToString(Seq.empty[Int])
val res0: String = (1 +: (2 +: (3 +: Nil)))
val res1: String = Nil

scala> seqToString(Vector(1, 2, 3))
     | seqToString(Vector.empty[Int])
val res2: String = (1 +: (2 +: (3 +: Nil)))
val res3: String = Nil

scala> seqToString(Map("one" -> 1, "two" -> 2, "three" -> 3).toSeq)
     | seqToString(Map.empty[String,Int].toSeq)
val res4: String = ((one,1) +: ((two,2) +: ((three,3) +: Nil)))
val res5: String = Nil

Beachte die übliche Redewendung für die Konstruktion einer leeren Sammlung, wie Vector.empty[Int]. Die Methoden von empty befinden sich in den Begleitobjekten.

Map ist kein Untertyp von Seq, weil es keine bestimmte Reihenfolge garantiert, wenn du darüber iterierst. Der Aufruf von Map.toSeq erzeugt eine Folge von Schlüssel-Wert-Tupeln, die zufällig in Einfügereihenfolge stehen, was ein Nebeneffekt der Implementierung für kleine Maps ist und nicht für beliebige Maps gilt. Die nicht leere Map Ausgabe zeigt sowohl die Klammern der Tupel als auch die von seqToString hinzugefügten Klammern.

Beachte die Ausgabe für die nicht leeren Seq (eigentlich List) und Vector. Sie zeigen die hierarchische Struktur, die eine verknüpfte Liste mit einem Kopf und einem Ende hat:

(1 +: (2 +: (3 +: Nil)))

Wir verarbeiten also Sequenzen mit nur zwei case Klauseln und Rekursion. Das impliziert etwas Grundlegendes über alle Sequenzen: Sie sind entweder leer oder nicht. Das klingt banal, aber wenn du solche grundlegenden Strukturmuster erkennst, hast du ein überraschend allgemeines Werkzeug zum "Teilen und Erobern". Das Idiom, das processSeq verwendet, ist weithin wiederverwendbar.

Um die Symmetrie zwischen Aufbau und Zerstörung zu demonstrieren, können wir die Ergebnisse der vorherigen Beispiele kopieren und einfügen, um die ursprünglichen Objekte zu rekonstruieren. Allerdings müssen wir die Zeichenketten in Anführungszeichen setzen:

scala> val is = (1 +: (2 +: (3 +: Nil)))
val is: List[Int] = List(1, 2, 3)

scala> val kvs = (("one",1) +: (("two",2) +: (("three",3) +: Nil)))
val kvs: List[(String, Int)] = List((one,1), (two,2), (three,3))

scala> val map = Map(kvs*)
val map: Map[String, Int] = Map(one -> 1, two -> 2, three -> 3)

Die Methode Map.apply erwartet eine wiederholte Parameterliste aus Tupeln mit zwei Elementen. Um die Sequenz kvs zu verwenden, benutzen wir das Idiom *, damit der Compiler die Sequenz in eine Liste mit wiederholten Parametern umwandelt.

Versuche, die Klammern zu entfernen, die wir in der vorangegangenen String-Ausgabe hinzugefügt haben.

Der Vollständigkeit halber sei erwähnt, dass es ein Analogon zu +: gibt, das verwendet werden kann, um die Sequenzelemente in umgekehrter Reihenfolge zu verarbeiten, :+:

// src/script/scala/progscala3/patternmatching/MatchReverseSeq.scala

scala> def reverseSeqToString[T](l: Seq[T]): String = l match
     |   case prefix :+ end => s"(${reverseSeqToString(prefix)} :+ $end)"
     |   case Nil => "Nil"

scala> reverseSeqToString(Vector(1, 2, 3, 4, 5))
val res6: String = (((((Nil :+ 1) :+ 2) :+ 3) :+ 4) :+ 5)

Beachte, dass Nil dieses Mal in der Ausgabe an erster Stelle steht. Ein Vector wird für die Eingabesequenz verwendet, um dich daran zu erinnern, dass der Zugriff auf ein Nicht-Kopf-Element O(1) für ein Vector ist, aber O(N) für ein List der Größe N! Folglich ist reverseSeqToString O(N) für eine Vector der Größe N und O(N2) für eine List der Größe N!

Wie zuvor kannst du diese Ausgabe nutzen, um die Sammlung zu rekonstruieren:

scala> val revList1 = (((((Nil :+ 1) :+ 2) :+ 3) :+ 4) :+ 5)
val revList1: List[Int] = List(1, 2, 3, 4, 5)       // but List is returned!

scala> val revList2 = Nil :+ 1 :+ 2 :+ 3 :+ 4 :+ 5  // unnecessary () removed
val revList2: List[Int] = List(1, 2, 3, 4, 5)

scala> val revList3 = Vector.empty[Int] :+ 1 :+ 2 :+ 3 :+ 4 :+ 5
val revList3: Vector[Int] = Vector(1, 2, 3, 4, 5)   // how to get a Vector

Mustervergleiche bei wiederholten Parametern

Apropos wiederholte Parameterlisten: Du kannst sie auch beim Mustervergleich verwenden:

// src/script/scala/progscala3/patternmatching/MatchRepeatedParams.scala

scala> def matchThree(seq: Seq[Int]) = seq match
     |   case Seq(h1, h2, rest*) =>    // same as h1 +: h2 +: rest => ...
     |     println(s"head 1 = $h1, head 2 = $h2, the rest = $rest")
     |   case _ => println(s"Other! $seq")

scala> matchThree(Seq(1,2,3,4))
     | matchThree(Seq(1,2,3))
     | matchThree(Seq(1,2))
     | matchThree(Seq(1))
head 1 = 1, head 2 = 2, the rest = List(3, 4)
head 1 = 1, head 2 = 2, the rest = List(3)
head 1 = 1, head 2 = 2, the rest = List()
Other! List(1)

Wir sehen eine weitere Möglichkeit, Sequenzen abzugleichen. Wenn wir rest nicht brauchen, können wir den Platzhalter _ verwenden, der case Seq(h1, h2, _*) ist. In Scala 2 wurde rest* als rest @ _* geschrieben. Die Syntax von Scala 3 ist konsistenter mit anderen Verwendungen von wiederholten Parametern.

Abgleich auf Tupel

Tupel lassen sich auch leicht mit ihrer literalen Syntax abgleichen:

// src/script/scala/progscala3/patternmatching/MatchTuple.scala

val langs = Seq(
  ("Scala",   "Martin", "Odersky"),
  ("Clojure", "Rich",   "Hickey"),
  ("Lisp",    "John",   "McCarthy"))

val results = langs.map {
  case ("Scala", _, _) => "Scala"                               1
  case (lang, first, last) => s"$lang, creator $first $last"    2
}
1

Entspricht einem Tupel mit drei Elementen, wobei das erste Element die Zeichenkette "Scala" ist und wir das zweite und dritte Argument ignorieren.

2

Finde ein beliebiges Tupel mit drei Elementen, wobei die Elemente von beliebigem Typ sein können, aber aufgrund der Eingabe langs auf Stringgeschlossen wird. Extrahiere die Elemente in die Variablen lang, first, und last.

Ein Tupel kann in seine einzelnen Elemente zerlegt werden. Wir können an jeder beliebigen Stelle innerhalb des Tupels auf literale Werte treffen und wir können Elemente ignorieren, die uns nicht interessieren.

In Scala 3 haben Tupel erweiterte Funktionen, die sie mehr wie verknüpfte Listen machen, bei denen aber der spezifische Typ jedes Elements erhalten bleibt. Vergleiche das folgende Beispiel mit der vorhergehenden Implementierung von seqToString, bei der *: den Operator +: ersetzt:

scala> langs.map {
     |   case "Scala" *: first *: last *: EmptyTuple =>
     |     s"Scala -> $first -> $last"
     |   case lang *: rest => s"$lang -> $rest"
     | }
val res0: Seq[String] = List(Scala -> Martin -> Odersky,
 Clojure -> (Rich,Hickey), Lisp -> (John,McCarthy))

Die Entsprechung von Nil für Tupel ist EmptyTuple. Die zweite Fallklausel kann jedes Tupel mit einem oder mehreren Elementen behandeln. Wir erstellen eine neue Liste, indem wir EmptyTuple selbst und ein Tupel mit einem Element voranstellen:

scala> val l2 = EmptyTuple +: ("Indo-European" *: EmptyTuple) +: langs
val l2: Seq[Tuple] = List((), (Indo-European,), (Scala,Martin,Odersky),
 (Clojure,Rich,Hickey), (Lisp,John,McCarthy))

scala> l2.map {
     |   case "Scala" *: first *: last *: EmptyTuple =>
     |     s"Scala -> $first -> $last"
     |   case lang *: rest => s"$lang -> $rest"
     |   case EmptyTuple => EmptyTuple.toString
     | }
val res1: Seq[String] = List((), Indo-European -> (),
 Scala -> Martin -> Odersky, Clojure -> (Rich,Hickey), Lisp -> (John,McCarthy))

Man könnte meinen, dass ("Indo-European") ausreicht, um ein Tupel mit einem Element zu bilden, aber der Compiler interpretiert die Klammern einfach als unnötige Umhüllung der Zeichenkette! ("Indo-European" *: EmptyTuple) erfüllt diesen Zweck.

So wie wir mit -> Paare (Zwei-Elemente-Tupel) konstruieren können, können wir sie auch auf diese Weise dekonstruieren:

// src/script/scala/progscala3/patternmatching/MatchPair.scala

val langs2 = Seq("Scala" -> "Odersky", "Clojure" -> "Hickey")

val results = langs2.map {
  case "Scala" -> _ => "Scala"                           1
  case lang -> last => s"$lang: $last"                   2
}
assert(results == Seq("Scala", "Clojure: Hickey"))
1

Passt auf ein Tupel mit der Zeichenkette "Scala" als erstem Element und irgendetwas als zweitem Element.

2

Übereinstimmung mit einem beliebigen anderen Tupel mit zwei Elementen.

Erinnere dich daran, dass ich gesagt habe, dass +: in Mustern eigentlich eine object im scala.collection Paket. Ebenso gibt es ein *: object und einen Typ-Alias für -> bis Tuple2.type (quasi das Pendant object für die Fallklasse Tuple2 ) im Paket scala.

Parameter Untupling

Betrachte dieses Beispiel mit Tupeln:

// src/script/scala/progscala3/patternmatching/ParameterUntupling.scala

val tuples = Seq((1,2,3), (4,5,6), (7,8,9))
val counts1 = tuples.map {    // result: List(6, 15, 24)
  case (x, y, z) => x + y + z
}

Ein Nachteil der Case-Syntax innerhalb der anonymen Funktion ist die Implikation, dass sie nicht erschöpfend ist, obwohl wir wissen, dass sie es für die Sequenz tuples ist. Außerdem ist es etwas umständlich, case hinzuzufügen. Scala 3 führt das Parameter-Untupling ein, das Spezialfälle wie diesen vereinfacht. Wir können das Schlüsselwort case weglassen:

val counts2 = tuples.map {
  (x, y, z) => x + y + z
}

Wir können sogar anonyme Variablen verwenden:

val counts3 = tuples.map(_+_+_)

Diese Aufhebung funktioniert jedoch nur für eine Ebene der Zerlegung:

scala> val tuples2 = Seq((1,(2,3)), (4,(5,6)), (7,(8,9)))
     | val counts2b = tuples2.map {
     |   (x, (y, z)) => x + y + z
     | }
     |
3 |  (x, (y, z)) => x + y + z
  |      ^^^^^^
  |      not a legal formal parameter

Benutze case für solche Fälle.

Wachen in Fallklauseln

Das Abgleichen von literalen Werten ist sehr nützlich, aber manchmal brauchst du ein wenig zusätzliche Logik:

// src/script/scala/progscala3/patternmatching/MatchGuard.scala

val results = Seq(1,2,3,4).map {
  case e if e%2 == 0 => s"even: $e"                          1
  case o             => s"odd:  $o"                          2
}
assert(results == Seq("odd:  1", "even: 2", "odd:  3", "even: 4"))
1

Passt nur, wenn e gerade ist.

2

Die einzige andere Möglichkeit ist, dass o ungerade ist.

Beachte, dass wir keine Klammern um die Bedingung im if Ausdruck brauchen, genauso wie wir sie in for comprehensions nicht brauchen. In Scala 2 galt dies auch für die Syntax der Guard-Klausel.

Abgleich auf Fallklassen und Enums

Es ist kein Zufall, dass dasselbe case Schlüsselwort für die Deklaration von Sonderklassen und für case Ausdrücke in match Ausdrücken verwendet wird. Die Eigenschaften von Fallklassen wurden entwickelt, um einen bequemen Mustervergleich zu ermöglichen. Der Compiler implementiert den Musterabgleich und die Extraktion für uns. Wir können sie mit verschachtelten Objekten verwenden und wir können Variablen auf jeder Ebene der Extraktion binden, was wir jetzt zum ersten Mal sehen:

// src/script/scala/progscala3/patternmatching/MatchDeep.scala

case class Address(street: String, city: String)
case class Person(name: String, age: Int, address: Address)

val alice   = Person("Alice",   25, Address("1 Scala Lane", "Chicago"))
val bob     = Person("Bob",     29, Address("2 Java Ave.",  "Miami"))
val charlie = Person("Charlie", 32, Address("3 Python Ct.", "Boston"))

val results = Seq(alice, bob, charlie).map {
  case p @ Person("Alice", age, a @ Address(_, "Chicago")) =>      1
    s"Hi Alice! $p"
  case p @ Person("Bob", 29, a @ Address(street, city)) =>         2
    s"Hi ${p.name}! age ${p.age}, in ${a}"
  case p @ Person(name, age, Address(street, city)) =>             3
    s"Who are you, $name (age: $age, city = $city)?"
}
assert(results == Seq(
  "Hi Alice! Person(Alice,25,Address(1 Scala Lane,Chicago))",
  "Hi Bob! age 29, in Address(2 Java Ave.,Miami)",
  "Who are you, Charlie (age: 32, city = Boston)?"))
1

Finde jede Person mit dem Namen "Alice", egal welchen Alters, an einer beliebigen Adresse in Chicago. Benutze p @, um die Variable p mit der gesamten Person zu verbinden und gleichzeitig Felder innerhalb der Instanz zu extrahieren, in diesem Fall age. Verwende a @, um die Variable a an die gesamte Instanz Address zu binden, während du auch die Felder street und city innerhalb der Instanz Address bindest.

2

Finde eine Person mit dem Namen "Bob", Alter 29, in einer beliebigen Straße und Stadt. Verbinde p mit der gesamten Instanz Person und a mit der verschachtelten Instanz Address.

3

Triff auf eine beliebige Person zu, indem du p mit der Instanz Person und name, age, street und city mit den verschachtelten Feldern verbindest.

Wenn du keine Felder aus der Instanz Person extrahierst, können wir einfach p: Person => schreiben...

Dieses verschachtelte Matching kann beliebig tief gehen. In diesem Beispiel wird der algebraische Datentyp enum Tree[T] aus "Aufzählungen und algebraische Datentypen" wieder aufgegriffen . Erinnere dich an die Definition von enum, die auch den "automatischen" Mustervergleich unterstützt:

// src/main/scala/progscala3/patternmatching/MatchTreeADTEnum.scala
package progscala3.patternmatching

enum Tree[T]:
  case Branch(left: Tree[T], right: Tree[T])
  case Leaf(elem: T)

Hier führen wir ein Deep Matching für bestimmte Strukturen durch:

// src/script/scala/progscala3/patternmatching/MatchTreeADTDeep.scala
import progscala3.patternmatching.Tree
import Tree.{Branch, Leaf}

val tree1 = Branch(
  Branch(Leaf(1), Leaf(2)),
  Branch(Leaf(3), Branch(Leaf(4), Leaf(5))))
val tree2 = Branch(Leaf(6), Leaf(7))

for t <- Seq(tree1, tree2, Leaf(8))
yield t match
  case Branch(
    l @ Branch(_,_),
    r @ Branch(rl @ Leaf(rli), rr @ Branch(_,_))) =>
      s"l=$l, r=$r, rl=$rl, rli=$rli, rr=$rr"
  case Branch(l, r) => s"Other Branch($l, $r)"
  case Leaf(x) => s"Other Leaf($x)"

Die gleiche Extraktion könnte für die alternative Version durchgeführt werden, die wir im ursprünglichen Beispiel mit einer versiegelten Klassenhierarchie definiert haben. Wir werden es in "Versiegelte Hierarchien und erschöpfende Übereinstimmungen" ausprobieren .

Die letzten beiden Fallklauseln sind relativ einfach zu verstehen. Die erste ist stark auf tree1 abgestimmt, obwohl sie _ verwendet, um einige Teile des Baums zu ignorieren. Beachte vor allem, dass es nicht ausreicht, l @ Branch zu schreiben. Wir müssen l @ Branch(_,_) schreiben. Wenn du hier (_,_) entfernst, wirst du feststellen, dass der erste Fall nicht mehr mit tree1 übereinstimmt, ohne dass es eine offensichtliche Erklärung dafür gibt.

Warnung

Wenn ein verschachteltes Muster match Ausdruck nicht übereinstimmt, stellen Sie sicher, dass Sie die vollständige Struktur erfassen, z. B. l @ Branch(_,_) statt l @ Branch.

Es lohnt sich, mit diesem Beispiel zu experimentieren, um verschiedene Teile der Bäume zu erfassen, damit du ein Gespür dafür entwickelst, was funktioniert und was nicht, und wie du match Ausdrücke debuggen kannst.

Hier ist ein Beispiel mit Tupeln. Stell dir vor, wir haben eine Folge von (String,Double) Tupeln für die Namen und Preise der Artikel in einem Geschäft und wollen sie mit ihrem Index ausgeben. Die Methode Seq.zipWithIndex ist hier sehr hilfreich:

// src/script/scala/progscala3/patternmatching/MatchDeepTuple.scala

val itemsCosts = Seq(("Pencil", 0.52), ("Paper", 1.35), ("Notebook", 2.43))

val results = itemsCosts.zipWithIndex.map {
  case ((item, cost), index) => s"$index: $item costs $cost each"
}
assert(results == Seq(
  "0: Pencil costs 0.52 each",
  "1: Paper costs 1.35 each",
  "2: Notebook costs 2.43 each"))

Beachte, dass zipWithIndex eine Folge von Tupeln der Form (element, index) oder in diesem Fall ((name, cost), index) zurückgibt. Wir haben diese Form abgeglichen, um die drei Elemente zu extrahieren und eine Zeichenkette aus ihnen zu erstellen. Ich schreibe oft Code wie diesen.

Abgleich mit regulären Ausdrücken

Reguläre Ausdrücke (oder Regexe) sind praktisch, um Daten aus Zeichenketten zu extrahieren, die eine bestimmte Struktur haben. Hier ist ein Beispiel:

// src/script/scala/progscala3/patternmatching/MatchRegex.scala

val BookExtractorRE = """Book: title=([^,]+),\s+author=(.+)""".r     1
val MagazineExtractorRE = """Magazine: title=([^,]+),\s+issue=(.+)""".r

val catalog = Seq(
  "Book: title=Programming Scala Third Edition, author=Dean Wampler",
  "Magazine: title=The New Yorker, issue=January 2021",
  "Unknown: text=Who put this here??"
)

val results = catalog.map {
  case BookExtractorRE(title, author) =>                             2
    s"""Book "$title", written by $author"""
  case MagazineExtractorRE(title, issue) =>
    s"""Magazine "$title", issue $issue"""
  case entry => s"Unrecognized entry: $entry"
}
assert(results == Seq(
  """Book "Programming Scala Third Edition", written by Dean Wampler""",
  """Magazine "The New Yorker", issue January 2021""",
  "Unrecognized entry: Unknown: text=Who put this here??"))
1

Entspricht einer Buchzeichenkette mit zwei Erfassungsgruppen (beachte die Klammern), eine für den Titel und eine für den Autor. Wenn du die Methode r für eine Zeichenkette aufrufst, wird daraus eine Regex erstellt. Auch eine Zeitschriftenzeichenkette mit Erfassungsgruppen für den Titel und die Ausgabe (Datum) wird abgeglichen.

2

Verwende die regulären Ausdrücke ähnlich wie die Fallklassen, wobei die Zeichenkette, auf die jede Capture-Gruppe passt, einer Variablen zugewiesen wird.

Da Regexe Backslashes für Konstrukte verwenden, die über die normalen ASCII-Steuerzeichen hinausgehen, solltest du entweder dreifach in Anführungszeichen gesetzte Zeichenketten für sie verwenden, wie in der Abbildung gezeigt, oder rohe interpolierte Zeichenketten, wie raw"foo\sbar".r. Andernfalls musst du diese Backslashes escapen; zum Beispiel "foo\\sbar".r. Du kannst auch reguläre Ausdrücke definieren, indem du neue Instanzen der Klasse Regex erstellst, wie in new Regex("""\W+""").

Warnung

Die Interpolation in Strings mit dreifachen Anführungszeichen funktioniert nicht sauber für die Regex-Escape-Sequenzen. Du musst diese Sequenzen immer noch escapen (z. B. s"""$first\\s+$second""".r statt s"""$first\s+$second""".r). Wenn du keine Interpolation verwendest, ist die Escape-Funktion nicht notwendig.

scala.util.matching.Regex definiert mehrere Methoden für andere Manipulationen, wie zum Beispiel das Suchen und Ersetzen von Übereinstimmungen.

Abgleich mit interpolierten Zeichenketten

Wenn du weißt, dass die Zeichenketten ein genaues Format haben, z. B. eine bestimmte Anzahl von Leerzeichen, kannst du sogar interpolierte Zeichenketten für den Mustervergleich verwenden. Lass uns die catalog wiederverwenden:

// src/script/scala/progscala3/patternmatching/MatchInterpolatedString.scala

val results = catalog.map {
  case s"""Book: title=$t, author=$a""" => ("Book" -> (t -> a))
  case s"""Magazine: title=$t, issue=$d""" => ("Magazine" -> (t -> d))
  case item => ("Unrecognized", item)
}
assert(results == Seq(
  ("Book", ("Programming Scala Third Edition", "Dean Wampler")),
  ("Magazine", ("The New Yorker", "January 2020")),
  ("Unrecognized", "Unknown: text=Who put this here??")))

Versiegelte Hierarchien und erschöpfende Übereinstimmungen

Gehen wir noch einmal auf die Notwendigkeit von erschöpfenden Übereinstimmungen ein und betrachten die Situation, in der wir eine enum oder die entsprechende sealed Klassenhierarchie haben.

Verwenden wir zunächst die enum Tree[T] Definition von vorhin. Wir können die Blätter und Zweige nach Mustern abgleichen, weil wir wissen, dass wir nie überrascht sein werden, etwas anderes zu sehen:

// src/script/scala/progscala3/patternmatching/MatchTreeADTExhaustive.scala
import progscala3.patternmatching.Tree
import Tree.{Branch, Leaf}

val enumSeq: Seq[Tree[Int]] = Seq(Leaf(0), Branch(Leaf(6), Leaf(7)))
val tree1 = for t <- enumSeq yield t match
  case Branch(left, right) => (left, right)
  case Leaf(value) => value
assert(tree1 == List(0, (Leaf(6),Leaf(7))))

Da es für einen Nutzer von Tree nicht möglich ist, einen weiteren case zu enum hinzuzufügen, können diese match Ausdrücke niemals brechen. Sie werden immer erschöpfend bleiben.

Als Übung ändere case Branch, um auf left und right zu rekursieren (du musst eine Methode definieren), und verwende dann ein tieferes Baumbeispiel.

Lass uns eine entsprechende versiegelte Hierarchie ausprobieren:

// src/main/scala/progscala3/patternmatching/MatchTreeADTSealed.scala
package progscala3.patternmatching

sealed trait STree[T]               // "S" for "sealed"
case class SBranch[T](left: STree[T], right: STree[T]) extends STree[T]
case class SLeaf[T](elem: T) extends STree[T]

Der Matchcode ist im Wesentlichen identisch:

import progscala3.patternmatching.{STree, SBranch, SLeaf}

val sealedSeq: Seq[STree[Int]] = Seq(SLeaf(0), SBranch(SLeaf(6), SLeaf(7)))
val tree2 = for t <- sealedSeq yield t match
  case SBranch(left, right) => (left, right)
  case SLeaf(value) => value
assert(tree2 == List(0, (SLeaf(6),SLeaf(7))))

Daraus ergibt sich, dass sealed Hierarchien und enums vermieden werden sollten, wenn sich die Typenhierarchie weiterentwickeln muss. Verwende stattdessen eine "offene" objektorientierte Typenhierarchie mit polymorphen Methoden anstelle von match Ausdrücken. Wir haben diesen Kompromiss in "Eine Beispielanwendung" besprochen .

Verkettung von Match-Ausdrücken

Scala 3 hat die Parsing-Regeln für match Ausdrücke geändert, um eine Verkettung zu ermöglichen, wie in diesem konstruierten Beispiel:

// src/script/scala/progscala3/patternmatching/MatchChaining.scala

scala> for opt <- Seq(Some(1), None)
     | yield opt match {
     |  case None => ""
     |  case Some(i) => i.toString
     | } match {  // matches on the String returned from the previous match
     |  case "" => false
     |  case _ => true
     | }
val res10: Seq[Boolean] = List(true, false)

Mustervergleiche außerhalb von Match-Ausdrücken

Der Mustervergleich ist nicht auf match Ausdrücke beschränkt. Du kannst sie auch in Zuweisungsanweisungen verwenden, den sogenannten Pattern Bindings:

// src/script/scala/progscala3/patternmatching/Assignments.scala

scala> case class Address(street: String, city: String, country: String)
scala> case class Person(name: String, age: Int, address: Address)

scala> val addr = Address("1 Scala Way", "CA", "USA")
scala> val dean = Person("Dean", 29, addr)
val addr: Address = Address(1 Scala Way,CA,USA)
val dean: Person = Person(Dean,29,Address(1 Scala Way,CA,USA))

scala> val Person(name, age, Address(_, state, _)) = dean
val name: String = Dean
val age: Int = 29
val state: String = CA

Sie arbeiten in for Verständnissen:

scala> val people = (0 to 4).map {
     |   i => Person(s"Name$i", 10+i, Address(s"$i Main Street", "CA", "USA"))
     | }
val people: IndexedSeq[Person] = Vector(Person(Name0,10,Address(...)), ...)

scala> val nas = for
     |   Person(name, age, Address(_, state, _)) <- people
     | yield (name, age, state)
val nas: IndexedSeq[(String, Int, String)] =
  Vector((Name0,10,CA), (Name1,11,CA), ...)

Angenommen, wir haben eine Funktion, die eine Folge von Doubles nimmt und die Anzahl, die Summe, den Durchschnitt, den Mindestwert und den Höchstwert in einem Tupel zurückgibt:

// src/script/scala/progscala3/patternmatching/AssignmentsTuples.scala

/** Return the count, sum, average, minimum value, and maximum value. */
def stats(seq: Seq[Double]): (Int, Double, Double, Double, Double) =
  assert(seq.size > 0)
  val sum = seq.sum
  (seq.size, sum, sum/seq.size, seq.min, seq.max)

val (count, sum, avg, min, max) = stats((0 until 100).map(_.toDouble))

Musterbindungen können mit interpolierten Zeichenketten verwendet werden:

// src/script/scala/progscala3/patternmatching/AssignmentsInterpStrs.scala

val str = """Book: "Programming Scala", by Dean Wampler"""
val s"""Book: "$title", by $author""" = str : @unchecked
assert(title == "Programming Scala" && author == "Dean Wampler")

Ich werde dir gleich erklären, warum du @unchecked brauchst.

Schließlich können wir Musterverknüpfungen mit einem regulären Ausdruck verwenden, um einen String zu zerlegen. Hier ist ein Beispiel für das Parsen von (einfachen!) SQL-Strings:

// src/script/scala/progscala3/patternmatching/AssignmentsRegex.scala

scala> val c = """\*|[\w, ]+"""  // cols
     | val t = """\w+"""         // table
     | val o = """.*"""          // other substrings
     | val selectRE =
     |   s"""SELECT\\s*(DISTINCT)?\\s+($c)\\s*FROM\\s+($t)\\s*($o)?;""".r

scala> val selectRE(distinct, cols, table, otherClauses) =
     |   "SELECT DISTINCT col1 FROM atable WHERE col1 = 'foo';": @unchecked
val distinct: String = DISTINCT
val cols: String = "col1 "
val table: String = atable
val otherClauses: String = WHERE col1 = 'foo'

In der Quelldatei findest du weitere Beispiele. Da ich die String-Interpolation verwendet habe, musste ich im letzten regulären Ausdruck zusätzliche Backslashes hinzufügen (z. B. \\s statt \s).

Als Nächstes erkläre ich, warum die @unchecked type-Anmerkung verwendet wurde.

Probleme bei der Bindung von Mustern

Generell solltest du bedenken, dass der Musterabgleich MatchError Ausnahmen auslöst, wenn die Übereinstimmung fehlschlägt. Das kann deinen Code anfällig machen, wenn du ihn in Zuweisungen verwendest, weil es schwieriger ist, sie erschöpfend zu machen. In den vorherigen Beispielen für interpolierte Strings und Regex kann der Typ String für die Werte auf der rechten Seite nicht sicherstellen, dass die Übereinstimmungen erfolgreich sind.

Angenommen, ich hätte die : @unchecked Typdeklaration nicht. In Scala 2 und 3.0 würden beide Beispiele ohne MatchErrorkompilieren und funktionieren. Ab einem zukünftigen Scala 3 Release oder beim Kompilieren mit -source:future schlagen die Beispiele fehl, zum Beispiel:

scala> val selectRE(distinct, cols, table, otherClauses) =
     |   "SELECT DISTINCT col1 FROM atable WHERE col1 = 'foo';"
     |
2 |  "SELECT DISTINCT col1 FROM atable WHERE col1 = 'foo';"
  |  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |pattern's type String is more specialized than the righthand side
  |expression's type String
  |
  |If the narrowing is intentional, this can be communicated by adding
  |`: @unchecked` after the expression.

Wenn du aber weißt, dass die Deklaration sicher ist, kannst du die @unchecked Typdeklaration hinzufügen, wie wir es bereits getan haben, und der Compiler wird sich nicht beschweren.

Wenn wir diese Warnungen jedoch zum Schweigen bringen, kann es sein, dass wir zur Laufzeit MatchErrors. Betrachte die folgenden Beispiele mit Sequenzen:

// src/script/scala/progscala3/patternmatching/AssignmentsFragile.scala

scala> val h4a +: h4b +: t4 = Seq(1,2,3,4) : @unchecked
val h4a: Int = 1
val h4b: Int = 2
val t4: Seq[Int] = List(3, 4)

scala> val h2a +: h2b +: t2 = Seq(1,2) : @unchecked
val h2a: Int = 1
val h2b: Int = 2
val t2: Seq[Int] = List()

scala> val h1a +: h1b +: t1 = Seq(1) : @unchecked     // MatchError!
scala.MatchError: List(1) (of class scala.collection.immutable.$colon$colon)
  ...

Seq schränkt die Anzahl der Elemente nicht ein, sodass die Übereinstimmungen auf der linken Seite funktionieren oder fehlschlagen können. Der Compiler kann zur Kompilierzeit nicht überprüfen, ob die Übereinstimmung erfolgreich ist oder eine MatchError auslöst. Daher wird er eine Warnung ausgeben, es sei denn, die @unchecked type annotation wird wie gezeigt hinzugefügt. Während die ersten beiden Fälle erfolgreich sind, löst der letzte Fall eine MatchError aus.

Pattern Matching als Filter für Comprehensions

In einem for Verständnis fungiert ein nicht erschöpfender Abgleich stattdessen als Filter:

// src/script/scala/progscala3/patternmatching/MatchForFiltering.scala

scala> val elems = Seq((1, 2), "hello", (3, 4), 1, 2.2, (5, 6))
val elems: Seq[Matchable] = List((1,2), hello, (3,4), 1, 2.2, (5,6))

scala> val what1 = for (case (x, y) <- elems) yield (y, x)        1
     | val what2 = for  case (x, y) <- elems  yield (y, x)
val what1: Seq[(Any, Any)] = List((2,1), (4,3), (6,5))
val what2: Seq[(Any, Any)] = List((2,1), (4,3), (6,5))
1

Das Schlüsselwort case ist für den Abgleich und die Filterung erforderlich. Die Klammern sind optional.

Beachte, dass der gefolgerte gemeinsame Supertyp für die Elemente in elems Matchable und nicht Any ist. Für what1 und what2 ist der abgeleitete Typ ein Tupel - ein Subtyp von Matchable. Die Tupelmitglieder können Any sein.

Das Schlüsselwort case war für Scala 2 oder 3.0 nicht erforderlich. Wenn du mit einer zukünftigen Version von Scala 3 beginnst oder mit -source:future kompilierst, wird die Warnung "narrowing" ausgelöst, wenn du das Schlüsselwort case weglässt:

scala> val nope = for (x, y) <- elems yield (y, x)
1 |val nope = for (x, y) <- elems yield (y, x)
  |               ^^^^^^
  |pattern's type (Any, Any) is more specialized than the right hand side
  |expression's type Matchable
  |
  |If the narrowing is intentional, this can be communicated by writing `case`
  |before the full pattern.
[source,scala]

Als wir zuvor über den erschöpfenden Abgleich gesprochen haben, haben wir ein Beispiel für eine Folge von Option Werten verwendet. Mithilfe des Musterabgleichs können wir Werte in einer Folge herausfiltern:

scala> val seq = Seq(None, Some(1), None, Some(2.2), None, None, Some("three"))
scala> val filtered = for case Some(x) <- seq yield x
val filtered: Seq[Matchable] = List(1, 2.2, three)

Musterabgleich und Löschung

Betrachten wir das folgende Beispiel, in dem wir versuchen, zwischen den Eingaben List[Double] und List[String] zu unterscheiden:

// src/script/scala/progscala3/patternmatching/MatchTypesErasure.scala

scala> val results = Seq(Seq(5.5,5.6,5.7), Seq("a", "b")).map {
     |   case seqd: Seq[Double] => ("seq double", seqd)   // Erasure warning
     |   case seqs: Seq[String] => ("seq string", seqs)   // Erasure warning
     |   case other             => ("unknown!", other)
     | }
2 |  case seqd: Seq[Double] => ("seq double", seqd)   // Erasure warning
  |       ^^^^^^^^^^^^^^^^^
  |       the type test for Seq[Double] cannot be checked at runtime
3 |  case seqs: Seq[String] => ("seq string", seqs)   // Erasure warning
  |       ^^^^^^^^^^^^^^^^^
  |       the type test for Seq[String] cannot be checked at runtime

Diese Warnungen resultieren aus der Typauslöschung, bei der die Informationen über die tatsächlichen Typen, die für die Typparameter verwendet werden, in der Compilerausgabe nicht erhalten bleiben. Während wir also zur Laufzeit feststellen können, dass das Objekt ein Seq ist, können wir nicht überprüfen, ob es ein Seq[Double] oder ein Seq[String] ist. Wenn wir die Warnung vernachlässigen, ist die zweite case Klausel für Seq[String] sogar unerreichbar. Die erste Klausel stimmt für alle Seqs überein.

Eine unschöne Abhilfe ist es, zuerst die Sammlung abzugleichen und dann einen verschachtelten Abgleich auf das Kopfelement zu verwenden, um den Typ zu bestimmen. Jetzt müssen wir auch mit einer leeren Sequenz umgehen:

// src/script/scala/progscala3/patternmatching/MatchTypesFix.scala

def doSeqMatch[T <: Matchable](seq: Seq[T]): String = seq match
  case Nil => ""
  case head +: _ => head match
    case _ : Double => "Double"
    case _ : String => "String"
    case _ => "Unmatched seq element"

val results = Seq(Seq(5.5,5.6), Nil, Seq("a","b")).map(seq => doSeqMatch(seq))
assert(results == Seq("Double", "", "String"))

Extraktoren

Wie funktioniert also der Mustervergleich und die Destrukturierung oder Extraktion? Scala definiert ein Paar object Methoden, die automatisch für Fallklassen und für viele Typen in der Scala-Bibliothek implementiert werden. Du kannst diese Extraktoren selbst implementieren, um das Verhalten für deine Typen anzupassen. Wenn diese Methoden für geeignete Typen verfügbar sind, können sie in Klauseln zur Mustererkennung verwendet werden.

Allerdings wirst du nur selten deine eigenen Extraktoren implementieren müssen. Du musst auch nicht die Details der Implementierung verstehen, um den Musterabgleich effektiv zu nutzen. Daher kannst du den Rest dieses Kapitels getrost überspringen und bei Bedarf später auf diese Diskussion zurückkommen.

unapply Methode

Erinnere dich daran, dass das Begleitobjekt einer Fallklasse mindestens eine Factory-Methode namens apply hat, die für die Konstruktion verwendet wird. Mit Hilfe von Symmetrieargumenten könnten wir daraus schließen, dass es eine weitere Methode namens unapply geben muss, die für die Dekonstruktion oder Extraktion verwendet wird. In der Tat gibt es eine Methode unapply, die in Pattern-Match-Ausdrücken für die meisten Typen aufgerufen wird.

Es gibt mehrere Möglichkeiten, unapply zu implementieren, insbesondere was zurückgegeben wird. Wir beginnen mit dem am häufigsten verwendeten Rückgabetyp: Option, der ein Tupel umhüllt. Dann besprechen wir andere Optionen für Rückgabetypen.

Betrachte noch einmal Person und Address von vorher:

person match
  case Person(name, age, Address(street, city)) => ...
  ...

Scala sucht nach Person.unapply(...) und Address.unapply(...) und ruft sie auf. Sie geben ein Option[(...)] zurück, wobei der Tupeltyp der Anzahl der Werte und ihrer Typen entspricht, die aus der Instanz extrahiert werden können.

Standardmäßig implementiert der Compiler für Case-Klassen unapply, um alle Felder zurückzugeben, die in der Argumentliste des Konstruktors deklariert sind. Das sind drei Felder für Person, vom Typ String, Int und Address, und zwei Felder für Address, beide vom Typ String. Das Begleitobjekt Person hat also Methoden, die wie folgt aussehen würden:

object Person:
  def apply(name: String, age: Int, address: Address) =
    new Person(name, age, address)
  def unapply(p: Person): Some[(String,Int,Address)] =
    Some((p.name, p.age, p.address))

Warum wird eine Option verwendet, wenn der Compiler bereits weiß, dass das Objekt eine Person ist? Scala erlaubt einer Implementierung von unapply, die Übereinstimmung aus irgendeinem Grund abzulehnen und None zurückzugeben. In diesem Fall versucht Scala, die nächste case Klausel zu verwenden. Außerdem müssen wir nicht alle Felder der Instanz offenlegen, wenn wir das nicht wollen. Wir können unsere age unterdrücken, wenn sie uns peinlich ist. Wir könnten sogar zusätzliche Werte zu den zurückgegebenen Tupeln hinzufügen.

Wenn eine Some, die ein Tupel umhüllt, von einer unapply zurückgegeben wird, extrahiert der Compiler die Tupel-Elemente für die Verwendung in der Fallklausel oder Zuweisung, z. B. für den Vergleich mit literalen Werten, die Bindung an Variablen oder das Weglassen für _ Platzhalter.

Es ist jedoch zu beachten, dass das einfache, vom Compiler erzeugte Person.unapply niemals fehlschlägt, so dass Some[...] als Rückgabetyp verwendet wird und nicht Option[...].

Die Methoden von unapply werden bei Bedarf rekursiv aufgerufen, d.h. die verschachtelte Instanz Address wird zuerst bearbeitet, dann Person.

Erinnere dich an den Ausdruck head +: tail, den wir zuvor verwendet haben. Jetzt wollen wir verstehen, wie er tatsächlich funktioniert. Wir haben gesehen, dass der Operator +: (cons) verwendet werden kann, um eine neue Sequenz zu erstellen, indem ein Element einer bestehenden Sequenz vorangestellt wird, und wir können auf diese Weise eine ganze Sequenz von Grund auf neu erstellen:

val list = 1 +: 2 +: 3 +: 4 +: Nil

Da +: eine Methode ist, die nach rechts bindet, stellen wir zuerst 4 der Liste Nil voran, dann 3 und so weiter.

Wenn die Konstruktion von Sequenzen mit einer Methode namens +: erfolgt, wie kann dann die Extraktion mit der gleichen Syntax erfolgen, damit wir eine einheitliche Syntax für Konstruktion und Dekonstruktion/Extraktion haben?

Zu diesem Zweck definiert die Scala-Bibliothek ein spezielles Singleton-Objekt namens +:. Ja, das ist der Name. Wie Methoden können auch Typen Namen mit einer Vielzahl von Zeichen haben.

Sie hat nur eine Methode, die Methode unapply, die der Compiler für unsere Anweisung case benötigt. Die Deklaration von unapply ist konzeptionell wie folgt (einige Details wurden entfernt):

def unapply[H, Coll](collection: Coll): Option[(H, Coll)]

Der Kopf ist vom Typ H, der gefolgert wird, und eine Sammlung vom Typ Coll, die den Typ der Schwanzsammlung darstellt. Es wird also ein Option eines Zwei-Elemente-Tupels mit dem Kopf und dem Schwanz zurückgegeben.

Wir haben in "Operatoren definieren" gelernt, dass Typen mit der Infix-Notation verwendet werden können. head +: tail ist also eine gültige Syntax, die +:(head, tail) entspricht. Tatsächlich können wir die normale Notation in einer Case-Klausel verwenden:

scala> def seqToString2[T](seq: Seq[T]): String = seq match
     |   case +:(head, tail) => s"($head +: ${seqToString2(tail)})"
     |   case Nil => "Nil"

scala> seqToString2(Seq(1,2,3,4))
val res0: String = (1 +: (2 +: (3 +: (4 +: Nil))))

Hier ist ein weiteres Beispiel, nur um den Punkt zu verdeutlichen:

// src/script/scala/progscala3/patternmatching/Infix.scala

infix case class And[A,B](a: A, b: B)

val and1: And[String,Int] = And("Foo", 1)
val and2: String And Int  = And("Bar", 2)
// val and3: String And Int  = "Baz" And 3  // ERROR

val results = Seq(and1, and2).map {
  case s And i => s"$s and $i"
}
assert(results == Seq("Foo and 1", "Bar and 2"))

Wir haben bereits erwähnt, dass du mit -> Musterpaare abgleichen kannst. Diese Funktion wird mit einem val implementiert, das in Predef, ->. Dies ist ein Alias für Tuple2.type, die den Subtyp Product2definiert eine unapply Methode, die für diese Mustervergleichsausdrücke verwendet wird.

Alternativen zu Optionsrückgabewerten

Während es üblich ist, einen Option von unapply zurückzugeben, ist jeder Typ mit der folgenden Signatur erlaubt, die auch Option implementiert:

def isEmpty: Boolean
def get: T

Es kann auch ein Boolean zurückgegeben werden oder ein Product Typ, der ein Supertyp von Tupeln ist, zum Beispiel. Hier ein Beispiel mit Boolean, bei dem wir zwischen zwei Arten von Zeichenketten unterscheiden wollen und der Abgleich wirklich eine Wahr/Falsch-Analyse implementiert:

// src/script/scala/progscala3/patternmatching/UnapplyBoolean.scala

object ScalaSearch:                                                  1
  def unapply(s: String): Boolean = s.toLowerCase.contains("scala")

val books = Seq(
  "Programming Scala",
  "JavaScript: The Good Parts",
  "Scala Cookbook").zipWithIndex   // add an "index"

val result = for s <- books yield s match                            2
  case (ScalaSearch(), index) => s"$index: found Scala"              3
  case (_, index) => s"$index: no Scala"

assert(result == Seq("0: found Scala", "1: no Scala", "2: found Scala"))
1

Definiere ein Objekt mit einer unapply Methode, die eine Zeichenkette annimmt, in Kleinbuchstaben umwandelt und das Ergebnis eines Prädikats zurückgibt; enthält sie "scala"?

2

Probiere es mit einer Liste von Zeichenketten aus, wobei die erste Groß-/Kleinschreibung nur erfolgreich ist, wenn die Zeichenkette "scala" enthält.

3

Leere Klammern erforderlich.

Es können auch andere Einzelwerte zurückgegeben werden. Hier ist ein Beispiel, das einen Scala Map in einen Java-Wert umwandelt HashMap:

// src/script/scala/progscala3/patternmatching/UnapplySingleValue.scala

import java.util.{HashMap as JHashMap}

case class JHashMapWrapper[K,V](jmap: JHashMap[K,V])
object JHashMapWrapper:
  def unapply[K,V](map: Map[K,V]): JHashMapWrapper[K,V] =
    val jmap = new JHashMap[K,V]()
    for (k,v) <- map do jmap.put(k, v)
    new JHashMapWrapper(jmap)

In Aktion:

scala> val map = Map("one" -> 1, "two" -> 2)
val map: Map[String, Int] = Map(one -> 1, two -> 2)

scala> map match
     |   case JHashMapWrapper(jmap) => jmap
val res0: java.util.HashMap[String, Int] = {one=1, two=2}

Es ist jedoch nicht möglich, einen ähnlichen Extraktor für Java's HashSet zu implementieren und sie in einem match Ausdruck zu kombinieren (weil es zwei mögliche Rückgabewerte gibt, nicht einen):

// src/script/scala/progscala3/patternmatching/UnapplySingleValue2.scala
scala> ...
scala> val map = Map("one" -> 1, "two" -> 2)
scala> val set = map.keySet
scala> for x <- Seq(map, set) yield x match
     |   case JHashMapWrapper(jmap) => jmap
     |   case JHashSetWrapper(jset) => jset
... errors ...

In der Quelldatei findest du alle Details. Für die Scala-Sammlungen gibt es bereits Werkzeuge zur Konvertierung zwischen Scala- und Java-Sammlungen. Siehe "Konvertierungen zwischen Scala- und Java-Sammlungen" für Details.

Eine weitere Option für unapply ist die Rückgabe eines ProductDas ist eine Abstraktion für Typen, bei denen es sinnvoll ist, die Mitgliedsfelder einheitlich zu behandeln, z. B. um sie per Index abzurufen oder über sie zu iterieren. Tupel implementieren Product. Wir können damit mehrere Rückgabewerte bereitstellen, die von unapply extrahiert werden:

// src/script/scala/progscala3/patternmatching/UnapplyProduct.scala

class Words(words: Seq[String], index: Int) extends Product:         1
  def _1 = words                                                     2
  def _2 = index

  def canEqual(that: Any): Boolean = ???                             3
  def productArity: Int = ???
  def productElement(n: Int): Any = ???

object Words:
  def unapply(si: (String, Int)): Words =                            4
    val words = si._1.split("""\W+""").toSeq                         5
    new Words(words, si._2)

val books = Seq(
  "Programming Scala",
  "JavaScript: The Good Parts",
  "Scala Cookbook").zipWithIndex   // add an "index"

val result = books.map {
  case Words(words, index) => s"$index: count = ${words.size}"
}
assert(result == Seq("0: count = 2", "1: count = 4", "2: count = 2"))
1

Jetzt brauchen wir eine Klasse Words, um die Ergebnisse zu speichern, wenn ein Spiel erfolgreich war. Words implementiert Product.

2

Definiere zwei Methoden zum Abrufen des ersten und zweiten Elements. Beachte, dass die Methodennamen dieselben sind wie bei den Zwei-Elemente-Tupeln.

3

Der Product -Trait deklariert diese Methoden ebenfalls, sodass wir zwar Definitionen bereitstellen müssen, aber keine funktionierenden Implementierungen benötigen. Das liegt daran, dass Product für unsere Zwecke eigentlich ein Marker-Trait ist. Alles, was wir wirklich brauchen, ist, dass Words diesen Typ mixin. Wir rufen also einfach die ??? Methode auf, die in Predef definiert ist und die immer einen NotImplementedError.

4

Passt auf ein Tupel von String und Int.

5

Teilt den String an Leerzeichen auf.

unapplySeq Methode

Wenn du eine Folge von extrahierten Elementen zurückgeben willst, anstatt einer festen Anzahl von ihnen, verwende unapplySeq. Es stellt sich heraus, dass das Seq Begleitobjekt apply und unapplySeq implementiert, aber nicht unapply:

def apply[A](elems: A*): Seq[A]
final def unapplySeq[A](x: Seq[A]): UnapplySeqWrapper[A]

UnapplySeqWrapper ist eine Hilfsklasse.

Das Matching mit unapplySeq wird in dieser Variante unseres vorherigen Beispiels für +: angewendet, bei dem wir ein gleitendes Fenster mit Paaren von Elementen auf einmal untersuchen:

// src/script/scala/progscala3/patternmatching/MatchUnapplySeq.scala

// Process pairs
def windows[T](seq: Seq[T]): String = seq match
  case Seq(head1, head2, tail*) =>                                   1
    s"($head1, $head2), " + windows(seq.tail)                        2
  case Seq(head, tail*) =>                                           3
    s"($head, _), " + windows(tail)
  case Nil => "Nil"                                                  4

val nonEmptyList   = List(1, 2, 3, 4, 5)
val emptyList      = Nil
val nonEmptyMap    = Map("one" -> 1, "two" -> 2, "three" -> 3)

val results = Seq(nonEmptyList, emptyList, nonEmptyMap.toSeq).map {
  seq => windows(seq)
}
assert(results == Seq(
  "(1, 2), (2, 3), (3, 4), (4, 5), (5, _), Nil",
  "Nil",
  "((one,1), (two,2)), ((two,2), (three,3)), ((three,3), _), Nil"))
1

Es sieht so aus, als würden wir Seq.apply(...) aufrufen, aber in einer Match-Klausel rufen wir tatsächlich Seq.unapplySeq auf. Wir nehmen die ersten beiden Elemente separat und den Rest der wiederholten Parameterliste als Ende.

2

Formatiere eine Zeichenkette mit den ersten beiden Elementen und verschiebe dann das Fenster um eins (nicht zwei), indem du seq.tail aufrufst, was auch head2 +: tail entspricht.

3

Wir brauchen auch eine Übereinstimmung für eine Ein-Element-Sequenz, z. B. am Ende, sonst haben wir keine vollständige Übereinstimmung. Diesmal verwenden wir den Schwanz in dem rekursiven Aufruf, obwohl wir eigentlich wissen, dass dieser Aufruf an windows(tail) einfach Nil zurückgibt.

4

Der Fall Nil beendet die Rekursion.

Wir könnten die zweite Fallanweisung umschreiben, um den letzten Aufruf von windows(tail)zu überspringen, aber ich habe es der Einfachheit halber so gelassen.

Wir könnten immer noch das +: Matching verwenden, was eleganter ist und was ich auch tun würde:

// src/script/scala/progscala3/patternmatching/MatchWithoutUnapplySeq.scala

val nonEmptyList   = List(1, 2, 3, 4, 5)
val emptyList      = Nil
val nonEmptyMap    = Map("one" -> 1, "two" -> 2, "three" -> 3)

// Process pairs
def windows2[T](seq: Seq[T]): String = seq match
  case head1 +: head2 +: _ => s"($head1, $head2), " + windows2(seq.tail)
  case head +: tail => s"($head, _), " + windows2(tail)
  case Nil => "Nil"

val results = Seq(nonEmptyList, emptyList, nonEmptyMap.toSeq).map {
  seq => windows2(seq)
}
assert(results == Seq(
  "(1, 2), (2, 3), (3, 4), (4, 5), (5, _), Nil",
  "Nil",
  "((one,1), (two,2)), ((two,2), (three,3)), ((three,3), _), Nil"))

Die Arbeit mit Schiebefenstern ist sogar so nützlich, dass Seq uns zwei Methoden gibt, um sie zu erstellen:

scala> val seq = 0 to 5
val seq: scala.collection.immutable.Range.Inclusive = Range 0 to 5

scala> seq.sliding(2).foreach(println)
ArraySeq(0, 1)
ArraySeq(1, 2)
ArraySeq(2, 3)
ArraySeq(3, 4)

scala> seq.sliding(3,2).foreach(println)
ArraySeq(0, 1, 2)
ArraySeq(2, 3, 4)

Beide sliding Methoden geben einen Iterator zurück, das heißt, sie sind faul und erstellen nicht sofort eine Kopie der Sammlung, was bei großen Sammlungen wünschenswert ist. Die zweite Methode benötigt ein stride Argument, das angibt, wie viele Schritte für das nächste Schiebefenster gemacht werden sollen. Die Vorgabe ist ein Schritt. Beachte, dass keines der Schiebefenster unser letztes Element, 5, enthält.

Implementierung von unapplySeq

Implementieren wir eine unapplySeq Methode, die an das vorangegangene Words Beispiel angepasst ist. Wir werden die Wörter wie zuvor tokenisieren, aber auch alle Wörter entfernen, die kürzer als ein bestimmter Wert sind:

// src/script/scala/progscala3/patternmatching/UnapplySeq.scala

object Tokenize:
  // def unapplySeq(s: String): Option[Seq[String]] = Some(tokenize(s))1
  def unapplySeq(lim_s: (Int,String)): Option[Seq[String]] =           2
    val (limit, s) = lim_s
    if limit > s.length then None
    else
      val seq = tokenize(s).filter(_.length >= limit)
      Some(seq)

  def tokenize(s: String): Seq[String] = s.split("""\W+""").toSeq      3

val message = "This is Programming Scala v3"
val limits = Seq(1, 3, 20, 100)

val results = for limit <- limits yield (limit, message) match
  case Tokenize() => s"No words of length >= $limit!"
  case Tokenize(a, b, c, d*) => s"limit: $limit => $a, $b, $c, d=$d"   4
  case x => s"limit: $limit => Tokenize refused! x=$x"

assert(results == Seq(
  "limit: 1 => This, is, Programming, d=ArraySeq(Scala, v3)",
  "limit: 3 => This, Programming, Scala, d=ArraySeq()",
  "No words of length >= 20!",
  "limit: 100 => Tokenize refused! x=(100,This is Programming Scala v3)"))
1

Wenn wir den Wert limit nicht abgleichen würden, würde die Erklärung folgendermaßen aussehen.

2

Wir passen auf ein Tupel mit dem Limit für die Wortgröße und die Wortfolge. Bei Erfolg geben wir Some(Seq(words)) zurück, wobei nach Wörtern mit einer Länge von mindestens limit gefiltert wird. Wir betrachten es als nicht erfolgreich und geben None zurück, wenn die Eingabe limit größer ist als die Länge der Eingabezeichenfolge.

3

Auf Leerzeichen aufteilen.

4

Erfasse die ersten drei zurückgegebenen Wörter und den Rest als wiederholte Parameterliste (d).

Versuche, dieses Beispiel zu vereinfachen, indem du keine Längenfilterung vornimmst. Entferne die Markierung in der Zeile für Kommentar 1 und arbeite von dort aus.

Rekapitulation und was kommt als Nächstes?

Zusammen mit for comprehensions macht Pattern-Matching idiomatischen Scala-Code prägnant und dennoch leistungsstark. Es bietet ein Protokoll, um Daten innerhalb von Datenstrukturen auf eine prinzipielle Art und Weise zu extrahieren, die du durch die Implementierung eigener unapply und unapplySeq Methoden steuern kannst. Mit diesen Methoden kannst du diese Informationen extrahieren und gleichzeitig andere Details verbergen. Die Informationen, die von unapply zurückgegeben werden, können sogar eine Transformation der tatsächlichen Felder in der Instanz sein.

Der Mustervergleich ist ein Markenzeichen vieler funktionaler Sprachen. Es ist eine flexible und prägnante Technik, um Daten aus Datenstrukturen zu extrahieren. Wir haben Beispiele für den Musterabgleich in case Klauseln gesehen und wie man den Musterabgleich auch in anderen Ausdrücken verwenden kann.

Das nächste Kapitel befasst sich mit einer einzigartigen, mächtigen, aber umstrittenen Funktion in Scala - den Kontextabstraktionen, die früher als Implicits bekannt waren. Dabei handelt es sich um eine Reihe von Werkzeugen zur Erstellung intuitiver DSLs, zur Reduzierung von Boilerplate und zur Erleichterung der Nutzung und Anpassung von APIs.

Get Scala programmieren, 3. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.