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 Array
s eingeführt. scala.IArray
. Array
ist 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 stattdessenMatchable
, 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
)
)
val
result
=
seq
.
map
{
case
1
=>
"int 1"
case
i
:
Int
=>
s"
other int:
$
i
"
case
d
:
(
Double
|
Float
)
=>
s"
a double or float:
$
d
"
case
"one"
=>
"string one"
case
s
:
String
=>
s"
other string:
$
s
"
case
(
x
,
y
)
=>
s"
tuple: (
$
x
,
$
y
)
"
case
unexpected
=>
s"
unexpected value:
$
unexpected
"
}
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)"
)
)
Wegen der Mischung der Werte ist
seq
vom TypSeq[Matchable]
.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
gleich1
ist. Wenn ja, geben wir einfach die Zeichenfolge"int 1"
zurück. Wenn der Wert ein andererInt
Wert ist, passt die nächste Klausel. In diesem Fall wird der Wert inInt
umgewandelt und der Variableni
zugewiesen, die zum Aufbau einer Zeichenkette verwendet wird.Übereinstimmung mit einem beliebigen
Double
oderFloat
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.Zwei Fallklauseln für Strings.
Finde ein Tupel mit zwei Elementen, wobei die Elemente von beliebigem Typ sind, und extrahiere die Elemente in die Variablen
x
undy
.Alle anderen Eingaben passen. Die Variable
unexpected
hat einen beliebigen Namen. Da keine Typdeklaration angegeben ist, wirdMatchable
abgeleitet. Dies fungiert als Standardklausel. Der boolesche Wert aus der Sequenzseq
wirdunexpected
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 Matchable
sind, 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
"
case
_
=>
s"
unexpected value:
$
x
"
}
assert
(
result2
==
Seq
(
"int: 1"
,
"int: 2"
,
"unexpected value: 3.14"
,
"unexpected value: one"
,
"unexpected value: (6,7)"
)
)
Verwende
_
für den Variablennamen, das heißt, wir erfassen ihn nicht.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
case
head
+:
tail
=>
s"
(
$
head
+:
${
seqToString
(
tail
)
}
)
"
case
Nil
=>
"Nil"
Definiere eine rekursive Methode, die eine
String
aus einerSeq[T]
für einen TypT
konstruiert, der aus der übergebenen Sequenz gefolgert wird. Der Körper ist ein einzelnermatch
Ausdruck.Es gibt zwei Übereinstimmungsklauseln und sie sind erschöpfend. Die erste passt auf jedes nicht leere
Seq
, wobei das erste Element alshead
und der Rest vonSeq
alstail
extrahiert wird. Dies sind gängige Bezeichnungen für die Teile einesSeq
, das über die Methodenhead
undtail
verfügt. Hier werden diese Begriffe jedoch als Variablennamen verwendet. Der Körper der Klausel konstruiert eineString
mit dem Kopf, gefolgt von+:
und dem Ergebnis des Aufrufs vonseqToString
am Ende, alles umgeben von Klammern,()
. Beachte, dass diese Methode rekursiv ist, aber nicht am Ende.Der einzige andere mögliche Fall ist ein leeres
Seq
. Wir können das Spezialfall-Objekt für einen leerenList
verwenden,Nil
verwenden, um alle leeren Fälle zu finden. Mit dieser Klausel wird die Rekursion beendet. Beachte, dass jeder Typ vonSeq
immer so interpretiert werden kann, dass er mitNil
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 Map
s 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"
case
(
lang
,
first
,
last
)
=>
s"
$
lang
, creator
$
first
$
last
"
}
Entspricht einem Tupel mit drei Elementen, wobei das erste Element die Zeichenkette "Scala" ist und wir das zweite und dritte Argument ignorieren.
Finde ein beliebiges Tupel mit drei Elementen, wobei die Elemente von beliebigem Typ sein können, aber aufgrund der Eingabe
langs
aufString
geschlossen wird. Extrahiere die Elemente in die Variablenlang
,first
, undlast
.
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"
case
lang
->
last
=>
s"
$
lang
:
$
last
"
}
assert
(
results
==
Seq
(
"Scala"
,
"Clojure: Hickey"
)
)
Passt auf ein Tupel mit der Zeichenkette "Scala" als erstem Element und irgendetwas als zweitem Element.
Ü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
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
"
case
o
=>
s"
odd:
$
o
"
}
assert
(
results
==
Seq
(
"odd: 1"
,
"even: 2"
,
"odd: 3"
,
"even: 4"
)
)
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"
)
)
=>
s"
Hi Alice!
$
p
"
case
p
@
Person
(
"Bob"
,
29
,
a
@
Address
(
street
,
city
)
)
=>
s"
Hi
${
p
.
name
}
! age
${
p
.
age
}
, in
${
a
}
"
case
p
@
Person
(
name
,
age
,
Address
(
street
,
city
)
)
=>
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)?"
)
)
Finde jede Person mit dem Namen "Alice", egal welchen Alters, an einer beliebigen Adresse in Chicago. Benutze
p @
, um die Variablep
mit der gesamtenPerson
zu verbinden und gleichzeitig Felder innerhalb der Instanz zu extrahieren, in diesem Fallage
. Verwendea @
, um die Variablea
an die gesamte InstanzAddress
zu binden, während du auch die Felderstreet
undcity
innerhalb der InstanzAddress
bindest.Finde eine Person mit dem Namen "Bob", Alter 29, in einer beliebigen Straße und Stadt. Verbinde
p
mit der gesamten InstanzPerson
unda
mit der verschachtelten InstanzAddress
.Triff auf eine beliebige Person zu, indem du
p
mit der InstanzPerson
undname
,age
,street
undcity
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
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
)
=>
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??"
)
)
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.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 enum
s 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
=
'fo
o
'
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 MatchError
kompilieren 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 MatchError
s. 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
)
|
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
)
)
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 Seq
s ü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 Product2
definiert 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
:
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
case
(
ScalaSearch
(
)
,
index
)
=>
s"
$
index
: found Scala
"
case
(
_
,
index
)
=>
s"
$
index
: no Scala
"
assert
(
result
==
Seq
(
"0: found Scala"
,
"1: no Scala"
,
"2: found Scala"
)
)
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"?Probiere es mit einer Liste von Zeichenketten aus, wobei die erste Groß-/Kleinschreibung nur erfolgreich ist, wenn die Zeichenkette "scala" enthält.
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 Product
Das 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
:
def
_1
=
words
def
_2
=
index
def
canEqual
(
that
:
Any
)
:
Boolean
=
???
def
productArity
:
Int
=
???
def
productElement
(
n
:
Int
)
:
Any
=
???
object
Words
:
def
unapply
(
si
:
(
String
,
Int
)
)
:
Words
=
val
words
=
si
.
_1
.
split
(
"""\W+"""
)
.
toSeq
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"
)
)
Jetzt brauchen wir eine Klasse
Words
, um die Ergebnisse zu speichern, wenn ein Spiel erfolgreich war.Words
implementiertProduct
.Definiere zwei Methoden zum Abrufen des ersten und zweiten Elements. Beachte, dass die Methodennamen dieselben sind wie bei den Zwei-Elemente-Tupeln.
Der
Product
-Trait deklariert diese Methoden ebenfalls, sodass wir zwar Definitionen bereitstellen müssen, aber keine funktionierenden Implementierungen benötigen. Das liegt daran, dassProduct
für unsere Zwecke eigentlich ein Marker-Trait ist. Alles, was wir wirklich brauchen, ist, dassWords
diesen Typ mixin. Wir rufen also einfach die???
Methode auf, die inPredef
definiert ist und die immer einenNotImplementedError
.Passt auf ein Tupel von
String
undInt
.
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
*
)
=>
s"
(
$
head1
,
$
head2
),
"
+
windows
(
seq
.
tail
)
case
Seq
(
head
,
tail
*
)
=>
s"
(
$
head
, _),
"
+
windows
(
tail
)
case
Nil
=>
"Nil"
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"
)
)
Es sieht so aus, als würden wir
Seq.apply(
...)
aufrufen, aber in einer Match-Klausel rufen wir tatsächlichSeq.unapplySeq
auf. Wir nehmen die ersten beiden Elemente separat und den Rest der wiederholten Parameterliste als Ende.Formatiere eine Zeichenkette mit den ersten beiden Elementen und verschiebe dann das Fenster um eins (nicht zwei), indem du
seq.tail
aufrufst, was auchhead2 +: tail
entspricht.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)
einfachNil
zurückgibt.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))
def
unapplySeq
(
lim_s
:
(
Int
,
String
)
)
:
Option
[
Seq
[
String
]
]
=
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
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
"
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)"
)
)
Wenn wir den Wert
limit
nicht abgleichen würden, würde die Erklärung folgendermaßen aussehen.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 mindestenslimit
gefiltert wird. Wir betrachten es als nicht erfolgreich und gebenNone
zurück, wenn die Eingabelimit
größer ist als die Länge der Eingabezeichenfolge.Auf Leerzeichen aufteilen.
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.