Pattern Matching lässt sich am ehesten mit der Java - Kontrollstruktur
switch
vergleichen. Pattern Matching ist jedoch weit
mächtiger und stellt sogar eine der Stützen der
funktionalen Programmierung mit Scala. Nachfolgend nochmals das Beispiel zu match
aus dem Abschnitt Elementare Kontrollstrukturen.
match
match
ist das Scala gegenstück zu Java's switch
,
wenn auch weit aus mächtiger. Mit Hilfe von match
können
in Scala auch Objekte zur Fallbestimmung herangezogen werden. Nachfolgend
ein Beispiel, wo Int-Objekte zur Fallunterscheidung herangezogen werden:
val iValue = 3 iValue match{ case 1 => println("eins") case 2 => println("zwei") case 3 => println("drei") case 4 => println("vier") case 5 => println("fuenf") case _ => println("nothing") }
Wird dieses Script ausgeführt, wird auf der Systemausgabe die Ausgabe
drei
ausgegeben.
Einem Java Programmierer könnte auffallen, dass vier
und fuenf
nicht ausgegeben werden. In Scala wird
die Angabe von einem break
impliziet vorgenommen.
Ein sogenanntes "fall through" kommt demnach nicht vor.
Das "Default" wird in Scala mit einem Unterstrich angegeben.
_ entspricht in Scala dem Wildcard (Platzhalter) und wird für
unbekannte Werte verwendet. Ein weiterer Unterschied zu Javas switch ist,
dass in einem match
Ausdruck einen case
"treffen" muss. Trifft kein case
zu, wird ein
MatchError
geworfen.
Um sich dem Pattern Matching zu näheren sei auch Kapitel 9, aus [42] Durchstarten mit Scala, empfohlen. Dort gibt es eine schöne und verständliche Einführung in die unterschiedlichen Typen der Mustererkennung.
Pattern Matching in Scala ist weitaus mehr als ein Ersatz für die switch
Kontrollstruktur in Java. Die Scala
Sprachspezifikation der Scala Version 2.9
unterscheidet folgende Typen des Pattern Matching 1:
Die unterschiedlichen Pattern Typen beziehen sich nicht auf einen kompletten match
-Ausdruck,
sondern nur auf einen einzelnen case
. Innerhalb eines match
-Ausdruckes kann für
jeden case
ein unterschiedlicher Pattern Typ verwendet werden. Wenn möglich schließt der
Scala Compiler cases aus, die niemals "matchen" können. So wird z.B. kein Type Pattern
auf einen String zugelassen, wenn der Compiler durch Typableitung feststellt, dass es sich um einen Int
Typ handeln muss.
Verwenden wir den Wildcard sprechen wir auch vom Wildcard-Pattern, dass einen Sondertyp des Variable Pattern
darstellt. Der Hauptunterschied liegt darin, dass wir bei der Verwendung einer Variablen,
auf das übergebene Objekt zugreifen können.
Es ist dabei zu beachten, dass der Variablenname mit einem Kleinbuchstaben beginnen muss.
Beim Variable Pattern wird, wie bei der Verwendung des Wildcards, jedes Objekt das Pattern "matchen". Weitere case
s
sind nach einem Variable Pattern unzulässig, da diese Fälle niemals eintreten können.
Das nachfolgende Beispiel zeigt ein Beispiel zum Variable Pattern,
wobei der "Default"-Fall der Variable defaultCase
zugewiesen wird. Nur der letzte case
entspricht dem Variable Pattern.
Alle anderen case
s entsprechen dem Literal Pattern.
object PMTest { def main(args: Array[String]) { testIt(2) testIt(3) testIt(4) testIt(5) testIt(1) } def testIt(integer: Int ){ integer match{ case 1 => println("found 1") case 2 => println("found 2") case 3 => println("found 3") case defaultCase => println("Default "+defaultCase) } } }
found 2 found 3 default 4 default 5 found 1
Beim Typed Pattern prüfen wir, ob das zu "matchende" Objekt von einem
bestimmten Typ ist. Ist dies der Fall, wird der entsprechende case
ausgeführt.
Ein Typed Pattern definieren wir, indem wir nach dem case
Schlüsselwort einen
Variablennamen angeben, welchen wir auf der rechten Seite des Pfeils zum Zugriff auf das Objekt
(ohne Typecast) verwenden können. Nach dem Variablennamen geben wir einen Doppelpunkt gefolgt
vom Namen der zu prüfenden Klasse an.
Nachfolgend ein Beispiel wo wir drei Typed Pattern definierten (Int
,
String
und A
).
object TypedPatternDemo { def main(args: Array[String]) { testObject(1) testObject("123") testObject(A(3.14)) testObject(B(6.28)) } def testObject(value: Any) { value match { case i: Int => println("Int") case s: String => println("String") case a: A => println("A hat den Wert: "+a.value) case _ => println("Wildcard") } } } case class A(value: Double) case class B(value: Double)
Die Ausführung des Programmes führt zu folgender Ausgabe auf der Systemausgabe:
Int String A hat den Wert: 3.14 Wildcard
Wie es der Name schon andeutet, vergleichen wir beim Literal Pattern das zu "matchende"
Objekt mit einem Literal. Das eingängigste Beispiel stellt der Vergleich mit Int
(Integer
)
Literalen dar. Dieser Vergleich entspricht am ehesten der switch
Kontrollstruktur
in Java.
object LiteralPattern { def main(args: Array[String]): Unit = { val v1: Int = 1 val v2: java.lang.Integer = 2 val v3: Int = 3 matchMe(v1) matchMe(v2) matchMe(v3) } def matchMe(any: Any): Unit = { any match { case 1 => println("Found value 1") case 2 => println("Found value 2") case 5 => println("Found value 3") case _ => println("No matching found") } } }
Die Ausführung des Programmes führt zu folgender Ausgabe auf der Systemausgabe:
Found value 1 Found value 2 No matching found
Das Literal Pattern ist nicht auf Int
(Integer
) Literale
beschränkt. Beim Literal Pattern können wir z.B. auch auf String
,
Char
und Boolean
Literale prüfen.
Im nachfolgenden Beispiel prüfen wir in den beiden ersten Cases auf Character
Literale, worauf ein String
Literal folgt. Zum Abschluss folgen
ein Int
und ein Boolean
Literal.
object LiteralPattern2 { def main(args: Array[String]): Unit = { val v1: Int = 1 val v2: Integer = 42 val v3: String = "Hallo" val v4: Char = 'b' val v5: Boolean = true matchMe(v1) matchMe(v2) matchMe(v3) matchMe(v4) matchMe(v5) } def matchMe(any: Any): Unit = { any match { case 'A' => println("Found value A") case 'b' => println("Found value b") case "Hallo" => println("Found value Hallo") case 42 => println("The solution") case true => println("Found value true") case _ => println("No matching found") } } }
Die Ausführung des Programmes führt zu folgender Ausgabe auf der Systemausgabe:
No matching found The solution Found value Hallo Found value b Found value true
Mithilfe des Constructor Pattern können wir einen zu matchenden Wert auf eine Case Class hin untersuchen. Dazu definieren wir einfach einen Case, wo wir den Konstruktor definieren, dem der zu matchende Wert entsprechen muss. Stimmen die Konstruktoren überein, wird der entsprechende Case ausgeführt.
Im nachfolgenden Beispiel definieren wir zunächst eine Case Class Person
mit den Konstruktorargumenten vorname
, nachname
und alter
.
Innerhalb der main
-Methode definieren wir drei Variablen vom Typ Person
,
welche wir an die Methode matchAPerson
übergeben. Innerhalb des match
-Ausdrucks
definieren wir zwei Constructor Pattern, welche auf eine Case Class des Typs
Person
prüfen und zum Abschluss ein Wildcard-Pattern.
case class Person(vorname: String, nachname: String, alter: Int) object TheMainClass { def main(args: Array[String]) : Unit = { val person1 = Person("Hans","Meyer",7) val person2 = Person("Heinz","Mustermann",28) val person3 = Person("Michaela","Schmidt",65) println(matchAPerson(person1)) println(matchAPerson(person2)) println(matchAPerson(person3)) } def matchAPerson(person: Person) : String = { person match { case Person("Hans","Meyer",7) => "Gefunden: Hans Meyer" case Person("Heinz","Mustermann",28) => "Gefunden: Heinz Mustermann" case _ => "Unbekannte Person" } } }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
Gefunden: Hans Meyer Gefunden: Heinz Mustermann Unbekannte Person
Interessiert uns ein oder mehrere Argumente der Case Class nicht, können wir diese mit einem Unterstrich belegen. Möchten wir ein Argument des Konstruktors auswerten, so können wir diesen mit einer Variablen belegen, die wir im "matchenden" Ausdruck auswerten können. Nachfolgend sehen wir uns das obige Beispiel nochmals abgewandelt an.
Im ersten case
prüfen wir, ob eine Person
übergeben wurde, die den
Vornamen "Hans" trägt. Alle weiteren Argumente interessieren uns nicht und können
auch nicht weiter ausgewertet werden. Im zweiten case
prüfen wir auf Vor- und Nachnamen
(Heinz Mustermann). Das Alter der Person weisen wir der Variablen alter
zu, welche
im folgenden Ausdruck ausgewertet wird.
case class Person(vorname: String, nachname: String, alter: Int) object TheMainClass { def main(args: Array[String]) : Unit = { val person1 = Person("Hans","Meyer",7) val person2 = Person("Heinz","Mustermann",28) val person3 = Person("Michaela","Schmidt",65) println(matchAPerson(person1)) println(matchAPerson(person2)) println(matchAPerson(person3)) } def matchAPerson(person: Person) : String = { person match { case Person("Hans",_,_) => "Gefunden: Hans" case Person("Heinz","Mustermann",alter) => "Heinz Mustermann ist "+alter+" Jahre alt" case _ => "Unbekannte Person" } } }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
Gefunden: Hans Heinz Mustermann ist 28 Jahre alt Unbekannte Person
Mithilfe des TuplePatterns können Tuple auf Ihren Inhalt untersuchen. Zur Definition des Tuple Patterns
geben wir einfach vor dem Schlüsselwort =>
den entsprechenden Konstruktor des Tuples an.
Anstelle konkreter Werte können wir Variablen verwenden, auf die wir im passenden Fall zugreifen
können. Das nachfolgende Beispiel zeigt die Verwendung des Tuple Patterns, wo wir prüfen, ob es
sich um ein Tuple2
, Tuple3
oder irgendetwas anderes handelt und geben (sofern getroffen)
den Inhalt des Tuples aus.
object TuplePatternTest { def main(args: Array[String]) : Unit = { val myTuple2 = ("T2",123) val myTuple3 = ("T3",456,"a") val myTuple4 = ("T4",789,23.4,'c') val mo = new MyObject println(mo.matchIt(myTuple2)) println(mo.matchIt(myTuple3)) println(mo.matchIt(myTuple4)) } } class MyObject { def matchIt(value: Any) : String = { value match{ case (a,b) => a+" "+b case (a,b,c) => a+" "+b+" "+c case _ => "Something else" } } }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
T2 123 T3 456 a Something else
Eine weitere mächtige Möglichkeit des Tuple Patterns ist, dass wir auf den Inhalt
des Tuples "matchen" können und so z.B. verschachtelte if
Anfragen
vermeiden können. Im nachfolgenden Beispiel "matchen" wir auf ein Tuple2
,
wo der zweite Wert 14 sein muss. In allen anderen Fällen trifft das
Wildcard Pattern.
object TuplePatternTest2 { def main(args: Array[String]) : Unit = { val person1 = ("Hans",13) val person2 = ("Michael",23) val person3 = ("Thomas",14) val person4 = ("Ralf",44) val mo = new MyObject2 println(mo.matchIt(person1)) println(mo.matchIt(person2)) println(mo.matchIt(person3)) println(mo.matchIt(person4)) } } class MyObject2 { def matchIt(value: Any) : String = { value match{ case (name,14) => "Hallo "+ name case _ => "Something else" } } }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
Something else Something else Hallo Thomas Something else
Abschließend sei noch angemerkt, dass wenn wir auf den Inhalt eines Tuple Elementes nicht zugreifen, keine Variable zu definieren brauchen. Wir geben in diesem Fall einfach den Unterstrich _ (Wildcard) als Platzhalter für das Element an.
def matchIt(value: Any) : String = { value match{ case (_,_,14) => "Hallo " case _ => "Something else" } }
Mithilfe von Pattern Guards können wir neben dem eigentlichen Pattern
weitere Bedingungen definieren, bevor der entsprechende Ausdruck ausgeführt wird.
Ein Pattern Guard wird mit dem Schlüsselwort if
direkt nach dem
Pattern eingeleitet. Dem if
folgt ein Ausdruck, dessen Ergebnis
ein Boolean
-Wert sein muss. Ergibt der Ausdruck den Wert true
,
so wird der entsprechende Ausdruck ausgeführt. Ergibt der Ausdruck den Wert false
,
wird der entsprechende Ausdruck nicht ausgeführt. Auf die runden Klammern des
Boolean
-Ausdrucks kann bei Pattern Guards verzichtet werden.
Das nachfolgende Beispiel zeigt den Einsatz von Pattern Guards. In den ersten beiden Pattern
wird auf einen Int
-Wert geprüft und im zutreffenden Fall an die
Variable i
gebunden. Im Anschluss wird mit einem Pattern Guard geprüft
ob der Wert, ohne Rest, durch 2 bzw. durch 3 teilbar ist.
object MainClass { def main(args: Array[String]): Unit = { matcherMethod(1) matcherMethod(2) matcherMethod(3) matcherMethod(4) matcherMethod(5) } def matcherMethod(any: Any): Unit = { any match { case i: Int if i % 2 == 0 => println("Int-Wert durch 2 ohne Rest teilbar") case i: Int if i % 3 == 0 => println("Int-Wert durch 3 ohne Rest teilbar") case _ => println("Wirldcard Pattern") } } }
Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:
Wirldcard Pattern Int-Wert durch 2 ohne Rest teilbar Int-Wert durch 3 ohne Rest teilbar Int-Wert durch 2 ohne Rest teilbar Wirldcard Pattern
Pattern Matching ist eine mächtige Kontrollstruktur. Aber falsch angewendet kann es zu schwer auffindbaren Fehlern kommen2. Nachfolgend ein Beispiel: Sehen Sie sich zunächst folgenden Quelltext an und bestimmen Sie, was beim Programmablauf auf der Systemausgabe ausgegeben wird.
object MainClass { def main(args: Array[String]) { new HasenFinder() } } case class Hase() class HasenFinder { def woLiegstDu(a: Any) = a match { case d: Hase => "Möhrenbeet" case Hase => "Pfeffer" case _ => "Kornfeld;" } val hase = Hase val ort = woLiegstDu(hase); println("Der Hase liegt im "+ort) }
Welche Ausgabe erwarten Sie? "Der Hase liegt im Möhrenbeet"? Nun, tatsächlich wird
"Der Hase liegt im Pfeffer" ausgegeben. Die Variable hase
ist keine Instanz
vom Typ Hase
, sondern vom Typ Hase.type
. Wenn Sie die Variable
hase
mit hase = Hase()
instanziieren (man beachte die hinzugekommene Klammer),
erhalten Sie als Ergebnis "Der Hase liegt im Möhrenbeet".