Einstieg

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")
}
itmapa.de - X2H V 0.5

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.

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.

Variable Patterns (dt. Variablen Muster) - Wildcard-Variable

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 cases 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 cases 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)
    }
  }
}
itmapa.de - X2H V 0.5
Die Ausführung des Programmes führt zu folgender Ausgabe auf der Konsole:
found 2
found 3
default 4
default 5
found 1
            

Typed Patterns (dt. Typmuster)

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)
itmapa.de - X2H V 0.11

Die Ausführung des Programmes führt zu folgender Ausgabe auf der Systemausgabe:

Int
String
A hat den Wert: 3.14
Wildcard
            

Literal Pattern (dt. Literalmuster)

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")
    }
  }
}
itmapa.de - X2H V 0.20

Die Ausführung des Programmes führt zu folgender Ausgabe auf der Systemausgabe:

Found value 1
Found value 2
No matching found

itmapa.de - X2H V 0.17

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")      
    }
  }
}
itmapa.de - X2H V 0.17

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
itmapa.de - X2H V 0.17

Constructor Pattern (dt. Konstruktoren Muster)

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"
    }
  }
}
itmapa.de - X2H V 0.15

Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:

Gefunden: Hans Meyer
Gefunden: Heinz Mustermann
Unbekannte Person
itmapa.de - X2H V 0.15

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"
    }
  }
}
itmapa.de - X2H V 0.17

Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:

Gefunden: Hans
Heinz Mustermann ist 28 Jahre alt
Unbekannte Person

itmapa.de - X2H V 0.17

Tuple Patterns (dt. Tuple-Muster)

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"
    }
  }
}
itmapa.de - X2H V 0.12

Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:

T2 123
T3 456 a
Something else
itmapa.de - X2H V 0.12

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"
    }
  }
}
itmapa.de - X2H V 0.12

Die Ausführung des Programms führt zu folgender Ausgabe auf der Systemausgabe:

Something else
Something else
Hallo Thomas
Something else
itmapa.de - X2H V 0.12

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"
  }
}
itmapa.de - X2H V 0.12

Pattern Guards

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")
    }
  }
}

itmapa.de - X2H V 0.17

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
itmapa.de - X2H V 0.17

Fallstrick: Wo liegt der Hase?

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)
}
itmapa.de - X2H V 0.11

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

______________________________
1 Siehe auch: [40]
2 Einen Dank an Michael fürs Entdecken und Marcus für die Lösung und den Hinweis.