Teil 8: Pattern Matching

Wir haben schon viel von Scala kennen gelernt, das bisherige Wissen reicht aber noch nicht um in Scala größere Programme schreiben zu können. Bevor wir endlich zur Objektorientierung kommen möchte ich euch noch das Thema Pattern Matching näher bringen.

In der einfachsten Form kann man Pattern Matching mit Mehrfachverzweigungen aus Java vergleichen. Sie sehen ähnlich aus und verhalten sich auch ähnlich, sind aber ungemein komfortabler.

int i = ...;
switch (i) {
  case 2:
    break;
  case 3: case 4: case 7:
    break;
  case 12:
    break;
  default:
    break;
}

Javas switch-case erlaubt lediglich das testen auf Zahlen, Enums und Strings (Java7). Das ist aber oft nicht ausreichend. Was wenn ich ein Objekt auf seinen Typ überprüfen möchte? Dann muss ich auf längere und vor allem hässliche if-else-instanceof-Kaskaden zurückgreifen. Auch Mehrfachvergleiche sind nicht besonders intuitiv. Man muss sich den fall-through zu Nutze machen und jede Überprüfung von neuem mit einem `case` einleiten. Der fall-through ist sowieso total unsinnig – habt ihr den jemals gebraucht? Also ich nicht und ich wäre glücklicher wenn man sich das `break` sparen könnte.

In Scala hat man sich dessen angenommen und die alte Mehrfachverzweigung durch das viel mächtigere Pattern Matching ersetzt:

val i = ...
i match {
  case 2 =>
  case 3 | 4 | 7 =>
  case 12 =>
  case _ =>
}

Das Schlüsselwort `switch` wurde durch `match` ersetzt und wird hinter die zu überprüfende Variable gesetzt. Mit case-Statements folgen dann die Überprüfungen. Die Überprüfung ist alles was sich zwischen dem case und dem `=>`-Symbol befindet. Das Symbol, das aussieht wie ein Pfeil hat dabei die gleiche Bedeutung wie der Punkt in Javas switch-case. So weit so gut, das war es aber auch schon mit den Gemeinsamkeiten.

Pattern Matching unterstützt kein fall-through. Die Ausführung einer Verzweigung endet mit Beginn einer Neuen – es wird also auch kein spezielles Schlüsselwort benötigt um das Ende zu kennzeichnen. Anhand des zweiten case-Statement ist zu erkennen, dass mehrere Überprüfungen komfortabel mit einem Statement gemacht werden können. Die zu überprüfenden Werte werden einfach mit einem senkrechten Strich voneinander getrennt – dem branchenüblichen Oder-Zeichen. Ein weiteres Merkmal ist, dass jedes überprüfte Element auf eine Überprüfung passen muss, ist dies nicht der Fall wird ein MatchError geworfen. Wenn also die Möglichkeit besteht, dass die Überprüfungen eines match-Statements nicht alle Werte abdecken können, dann muss ein default-Fall eingefügt werden. Diesen erstellt man in dem man auf den Unterstrich matcht. Wir erinnern uns: Der Unterstrich steht für irgendwas, er kann alles sein. Er wird immer dann gematcht, wenn kein nichts anderes passt. Das Pattern Matching wird beendet sobald korrekt gematcht wurde. Nach der Abarbeitung eines erfolgreichen case werden die restlichen case also nicht mehr ausgeführt. Dies bedeutet, dass der default-Fall immer als letztes geprüft werden muss, alles was danach kommt ist nämlich unerreichbar. War doch gar nicht so schwer, oder? Wir müssen weniger Code schreiben um das gleiche zu erreichen wie Javas switch-case.

Aber das war erst der Anfang.

Pattern Matching kann auf beliebige Typen prüfen – schauen wir uns dazu gleich das nächste Beispiel an:

val any: Any = ...
val matched = any match {
  case 2 | 4 | 7 => "a number"
  case "hello" => "a string"
  case true | false => "a boolean"
  case 45.35 => "a double"
  case _ => "an unknown value"
}

Wir haben irgendein Any und wollen wissen was genau es ist – kein Problem. Dadurch, dass in Scala alles ein Wert zurück gibt haben wir die Möglichkeit die letzte Anweisung innerhalb eines case-Blocks an eine Variable zu binden. Bitte beachtet, dass wir keine geschweiften Klammern benötigen um innerhalb eines match einen Block zu kennzeichnen – anhand des `=>` und des case kann der Compiler zuverlässig das Vorhandensein eines solchen Blocks erkennen:

val matched = any match {
  case 2 | 4 | 7 =>
    doSomething()
    "a number"
  case "hello" =>
    doSomething()
    "a string"
  case true | false =>
    doSomething()
    "a boolean"
  case 45.35 =>
    doSomething()
    "a double"
  case _ =>
    doSomething()
    "an unknown value"
}

Durch die Typinferenz erkennt der Compiler, dass die Variable `matched` vom Type String ist – ich hoffe zwar, dass ihr euch das mittlerweile selbst zusammenreimen könnt, aber man kann es ja nie oft genug erwähnen.

Das war bisher ja alles ganz nett, aber besonders vom Stuhl haut es uns nicht gerade. Das kann aber noch kommen, denn match kann noch mehr. Was ist wenn wir z.B. innerhalb eines gematchten Statements auf das zugreifen wollen was gematcht wurde? Anstatt also bei einer gematchten Zahl nur zurück zu geben, dass wir eine Zahl haben wäre es doch schön auch den Wert dieser Zahl zurückzugeben. Wir könnten jetzt umständlich das zu matchende Objekt casten um dann auf dessen Methoden zugreifen zu können, aber warum sollten wir das tun wenn Scala das schon für uns erledigt?

val matched = any match {
  case n: Int => "a number with value: "+n
  case _: String => "a string"
  case true | false => "a boolean"
  case d @ 45.35 => "a double with value "+d
  case d => "an unknown value "+d
}

Wir haben die Möglichkeit anstatt auf die Instanz eines bestimmten Typs auch einfach nur auf den Typ zu überprüfen. Dazu müssen wir bloß einen einen Identifier hinter das case schreiben und den Typ mit einem Doppelpunkt an den Identifier binden, wie im ersten case-Statement der Fall. Wir könnten das so lesen: Prüfe ob das gematchte Objekt vom Typ Int ist und wenn ja, dann binde das Objekt an eine Variable namens `n` und weise ihr den Typ Int zu. Es genügt nur den Namen der Variable anzugeben, wie bei den Generatoren und Definitionen der for-Expression benötigen wir kein `val` oder `var`. Die Sichtbarkeit dieser Variable beschränkt sich auf das case-Statement wir können also bei mehreren Überprüfungen auch mehrmals den gleichen Variablennamen nehmen – wie zu sehen bei den letzten beiden Statements.

Sollten wir nur auf eine beliebige Instanz eines Typs prüfen wollen, so besteht die Möglichkeit den Variablennamen wegzulassen und statt dessen einen Unterstich zu nehmen wie beim zweiten case-Statement gezeigt.

Eine weitere Besonderheit stellt das `@`-Zeichen dar. Es bedeutet so viel wie: Prüfe ob ein Objekt gleich der Instanz eines Typs ist und wenn ja, dann binde diese Instanz an einen Identifier. Im obigen Beispiel prüfen wir ob ein Double mit dem Wert 45.35 vorliegt. Sollte das der Fall sein wird dieser Wert an die Variable `d` gebunden, die wir dann benutzen können.

Um den Unterschied zwischen `@` und `:` nochmal klar und deutlich hervorzuheben: Ersteres prüft auf eine Instanz, letzteres auf einen Typ.

Neben einem Unterstich haben wir die Möglichkeit im letzten Statement auch einfach einen Identifier zu benutzen. Das können wir immer dann machen wenn wir noch auf das gematchte Objekt zugreifen wollen.

Schauen wir uns das ganze also mal in Aktion an:

def matching(any: Any) = any match {
  case n: Int => "a number with value: "+n
  case _: String => "a string"
  case true | false => "a boolean"
  case d @ 45.35 => "a double with value 45.35"
  case d => "an unknown value "+d
}

scala> matching(734)
res0: java.lang.String = a number with value: 734

scala> matching("hello")
res1: java.lang.String = a string

scala> matching(true)
res2: java.lang.String = a boolean

scala> matching(45.35)
res3: java.lang.String = a double with value 45.35

scala> matching(Nil)
res4: java.lang.String = an unknown value List()

Funktioniert alles. Wenn wir die bisher kennen gelernte Möglichkeiten miteinander kombinieren, können wir richtig tolle Objekte überprüfen:

def matching(xs: List[Int]) = xs match {
  case 5 :: 3 :: Nil => "List contains 5 and 3"
  case _ :: 7 :: _ => "Second element of List is 7"
  case List(1, tail @ _*) => "first element is 1 and tail is: "+tail
  case Nil => "Nil"
}

Im ersten Fall prüfen wir ob die Liste zwei Elemente besitzt (Nil wird nicht als Element angesehen, es ist einfach nur das Ende), im zweiten Fall ob das zweite Element 7 ist. Mit dem `::`-Symbol können wir die Liste einfach zusammen bauen, das ist uns bereits bekannt.

Pattern Matching erlaubt uns Objekte beliebiger Komplexitätsstufe zusammenzubauen indem wir einfach den Konstruktor mit den Elementen, die uns interessieren, aufrufen. So geschehen im dritten Fall. Das erste Element dort soll 1 sein, der Rest wird an die Variable `tail` gebunden. Die dort auftauchende Symbole sollten uns nicht unbekannt sein. Wir haben eine so ähnliche Schreibweise schon mal in Verbindung mit varargs gesehen. Hier heißt es so viel wie: Binde beliebig viele Objekte an die Variable `tail`.

Diese Schreibweise leuchtet euch nicht ein? Gut, mir auch nicht. 😉
Einfacher wäre es `List(1, tail*)` zu schreiben, dies funktioniert aber aufgrund eines Bugs im Compiler nicht. Ein Bug-Report dazu ist schon vorhanden, jetzt muss er nur noch umgesetzt werden. Bis es soweit ist müssen wir uns mit der etwas umständlicheren Variante abfinden.

Guards

Eine weitere Eigenschaft des Pattern Matching sind die sogenannten Guards. Sie funktionieren ähnlich wie `filter` aus den for-Expressions:

def matching(any: Any) = any match {
  case n: Int  if n > 10 && n < 100 => "int"
  case d: Double if scala.math.round(d) == 20 => "double"
  case s: String => if (s.size != 6) "string" else s
  case _ => "unknown"
}

Mit Hilfe der Guards können wir überprüfen die Überprüfungen noch weiter einschränken. Bitte beachtet, dass es einen Unterschied macht ob wir einen Guard (linke Seite von `=>`) oder eine if-Expression (rechte Seite von `=>`) verwenden. Die rechte Seite wird erst ausgeführt wenn das Muster auf der linken Seite passt. Das bedeutet, dass die rechte Seite auf jeden Fall einen else-Zweig besitzen muss, auf der linken Seite dagegen wird bei einem else-Fall einfach zum nächsten Muster weiter gesprungen:

scala> matching(56)
res20: java.lang.String = int

scala> matching(128)
res21: java.lang.String = unknown

scala> matching(20.345)
res22: java.lang.String = double

scala> matching("myname")
res23: java.lang.String = myname

scala> matching("hello")
res24: java.lang.String = string

Ich bin groß und du bist klein

In Verbindung mit Pattern Matching gibt es doch tatsächlich etwas wichtiges zu beachten: Es ist nicht egal ob wir einen gematchten Wert groß oder klein schreiben. Das Einhalten der Naming-Conventions (Variablen „lowerCamelCase“, Objekte „UpperCamelCase“) ist dient also nicht nur dem Verständnis des Codes, es dient auch zum Vorbeugen von Fehlern.

Schauen wir uns das mal genauer an:

scala> 5 match { case a: Int => "int" case _ => "unknown" }
res26: java.lang.String = int

scala> 5 match { case A: Int => "int" case _ => "unknown" }
<console>:1: error: '=>' expected but ':' found.
       5 match { case A: Int => "int" case _ => "unknown" }
                       ^

Da der Compiler den Anfang und das Ende eines Blocks bei den case-Statements selbst erkennen kann müssen wir die einzelnen case-Statements nicht mit einem Strichpunkt voneinander trennen.

Beim Code mit einem großgeschriebenen Variablennamen beschwert sich der Compier. Der Grund ist, dass der Compiler durch den großen Anfangsbuchstaben nun keine Variable mehr erwartet, sondern einen Extraktor! Er versucht den Extraktor der Klasse A aufzurufen. Da A ja aber schon ein Typ ist dürfen wir keinen weiteren Typ mehr angeben (was wir durch das `: Int` aber tun). So kommt es zur Fehlermeldung. Es ist also unerlässlich, dass Variablen immer klein geschrieben werden.

Auf ein weiteres Problem mit den Naming-Conventions stoßen wir wenn wir versuchen auf den Inhalt einer bereits existierenden Variable zu matchen:

scala> val a = 5
a: Int = 5

scala> 7 match { case a => "5" case _ => "unknown" }
<console>:8: error: unreachable code
              7 match { case a => "5" case _ => "unknown" }
                                                ^

Der Compiler beschwert sich wegen unerreichbarem Code, warum? Wir glauben, dass wir den Inhalt der Variable `a` prüfen, das stimmt aber nicht. Das Pattern Matching Konstrukt erzeugt eine neue Variable a und bindet an diese jeden Wert, den wir eingeben. Dadurch kann das zweite Muster nie erreicht werden, da schon das erste true ergibt. Wollen wir auf den Wert einer Variable prüfen müssen wir diese in Backticks schreiben:

scala> 7 match { case `a` => "5" case _ => "unknown" }
res32: java.lang.String = unknown

scala> 5 match { case `a` => "5" case _ => "unknown" }
res33: java.lang.String = 5

Damit teilen wir dem Compiler mit, dass er keine neuen Variable mehr erzeugen, sondern stattdessen eine schon vorhandene dereferenzieren soll.

Backticks kommen auch zum Einsatz wenn wir z.B. eine Methode aus einer Java-Lib aufrufen wollen, die gleich heißt wie ein Schlüsselwort in Scala. So können wir nicht

obj.match(param)

schreiben, da der Compiler die Methode match nicht als Methode sondern als Schlüsselwort erkennen würde. Die Schreibweise

obj.`match`(param)

funktioniert dagegen.

Pattern Matching everywhere

Eine der größten stärken von Pattern Matching ist sicherlich, dass wir es überall anwenden können, nicht nur innerhalb eines match-Blocks:

scala> val head :: tail = List(1, 2, 3)
head: Int = 1
tail: List[Int] = List(2, 3)

Was wir hier machen ist eigentlich ganz einfach. Wir haben eine Liste und binden dessen Elemente an verschiedene Variablen. Das macht besonders viel Sinn, wenn wir eine Methode haben, die komplexe Objekte zurück gibt und wir deren Werte an eine Variable binden wollen.

Um nochmal das Beispiel mit den Tuplen aus einem vorherigen Artikel aufzugreifen:

def heavyCalculation() = {
  val memoryUsage = 50
  val cpuUsage = 91
  val networkUsage = 31
  (memoryUsage, cpuUsage, networkUsage)
}

Die naheliegendste Version unseres Ziels würden wir jetzt ungefähr so erreichen:

scala> val usage = heavyCalculation()
usage: (Int, Int, Int) = (50,91,31)

scala> val memoryUsage = usage._1
memoryUsage: Int = 50

scala> val cpuUsage = usage._2
cpuUsage: Int = 91

scala> val networkUsage = usage._3
networkUsage: Int = 31

Aber warum nicht einfach folgendes schreiben?

scala> val (memoryUsage, cpuUsage, networkUsage) = heavyCalculation()
memoryUsage: Int = 50
cpuUsage: Int = 91
networkUsage: Int = 31

Was ist wohl einfacher und übersichtlicher? Wir erzeugen einfach einen Tuple3 und prüfen mit Pattern Matching ob wir der Rückgabewert der Methode an eben diesen Tuple3 binden können. Ist dies der Fall werden die Werte dann an die Variablen gebunden und wir können schön unkompliziert weiter programmieren.

Das können wir uns auch innerhalb von for-Expressions zu nutze machen:

scala> val m = Map(1 -> "a", 2 -> "b", 3 -> "c")
m: scala.collection.immutable.Map[Int,java.lang.String] = Map(1 -> a, 2 -> b, 3 -> c)

scala> for ((pos, letter) <- m) println("the letter '"+letter+"' has the "+pos+". position in the alphabet")
the letter 'a' has the 1. position in the alphabet
the letter 'b' has the 2. position in the alphabet
the letter 'c' has the 3. position in the alphabet

Da die Map uns über lauter Tuple2 iterieren lässt können wir diese auch einfach auspacken und dann innerhalb der for-Expression damit arbeiten.

Das Prinzip funktioniert auch mit anderen Objekten gleich wie beim Tuple:

scala> val seq = Seq(Seq(1, 2, 3), Seq(4, 5, 6), Seq(7, 8, 9))
seq: Seq[Seq[Int]] = List(List(1, 2, 3), List(4, 5, 6), List(7, 8, 9))

scala> val Seq(Seq(_*), middle @ Seq(4, 5, 6), Seq(_*)) = seq
middle: Seq[Int] = List(4, 5, 6)

Wir können die Seq so weit auseinanderbauen wie wir wollen und uns die Daten herausgreifen, die wir brauchen.

Wenn wir nur `_*` benutzen funktioniert das Matching, ein `x*` dagegen verurscht den Compilerfehler. Hier müssen wir also wieder zu `x @ _*` greifen.

Man kann mit Pattern Matching noch ein paar andere Sachen machen. Welche das sind und vor allem wie wir uns selbst eigene Extraktoren bauen können werden ich euch später erklären, nachdem wir die Objektorientierung hinter uns haben. Eigene Extraktoren würden uns z.B. erlauben so etwas zu schreiben:

val Person(name, address @ Address(city, zip, street)) = ...

Hier haben wir die zwei Klassen Person und Address und wollen an deren Inhalt rankommen. Sieht interessant aus, oder?

Advertisements

5 comments so far

  1. Alexander Tyuryaev on

    Gibt es n Java-Switch einen Punkt?

  2. Adako on

    Sicherlich wurde dort ein Doppelpunkt gemeint.

  3. Connz on

    Der 3. Fall des matching Beispiels geht so doch etwas „schöner“ oder?

    def matching(xs: List[Int]) = xs match {

    case 1 :: tail => „first element is 1 and tail is: „+tail

    }

    • Simon Schäfer on

      Ja stimmt, das ist etwas schöner. Ich weiß gar nicht mehr warum ich die andere Variante gewählt habe, vermutlich wollte ich zeigen, dass das auch geht.


Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s

%d Bloggern gefällt das: