Musterlösung Patternmatching


Bei vielen Scala-Lernenden scheint es drei Phasen beim Umgang mit Patternmatching zu geben: Skepsis, Hype und Normalität. Zuerst begegnen viele OO-Vorbelastete diesem Feature mit Skepsis. Martin Odersky beschreibt diese Ressentiments sehr schön in seinem Artikel „In Defense of Pattern Matching“. Nachdem man die Nützlichkeit (und die Ungültigkeit der Gegenargumente) verstanden hat, wird sinnlos alles gematcht, was einem vor die Flinte kommt. Schließlich findet man früher oder später das rechte Maß, wobei Denkanstöße wie Tony Morris‘ Option Cheat Sheet zeigen, dass Patternmatching eben nicht immer die beste Lösung ist.

Heute wollen wir einmal ganz systematisch verschiedene Details des Patternmatching beleuchten. Prinzipiell ähnelt Patternmatching einem switch-Statement in Java, und kann auch so benutzt werden. Die drei Hauptunterschiede sind, dass kein „Durchfallen“ zum nächsten Fall möglich ist (was in Java mit break verhindert werden muss), dass match im Gegensatz zu Javas switch einen Wert zurückliefert, und dass ein MatchError ausgelöst wird, wenn keines der Muster passt. Statt default schreibt man case _. Die Muster werden wie in Java von oben nach unten abgearbeitet, und das erste passende „gewinnt“ (selbst wenn noch weitere passen würden):

def test(ch: Char) = ch match {
  case 'a' => "Vokal a"
  case 'e' => "Vokal e"
  case 'i' => "Vokal i"
  case 'o' => "Vokal o"
  case 'u' => "Vokal u"
  case _ => "Konsonant"
}
//--> test: (Char)java.lang.String

println(test('u'))
//--> Vokal u
println(test('v'))
//--> Konsonant

Soll ein Fall für mehrere Muster gelten, kann man diese mit | (was ja sonst auch „oder“ bedeutet) verknüpfen:

def test(ch: Char) = ch match {
  case 'a' | 'e' | 'i' | 'o' | 'u'=> "Vokal"
  case _ => "Konsonant"
}

Mit Patternmatching hat man auch ein sicheres und bequemes Äquivalent zu Javas berüchtigten instanceof-Kaskaden. Dazu gibt man als Muster eine Variable an und hinter dem Doppelpunkt ihren Typ. Lässt man den Typ weg, passt die Variable auf alle ankommenden Werte. Interressiert nur der Typ, aber nicht der Wert, kann man _ : Typ schreiben:

def test(any: Any) = any match {
  case s: String => "String " + s
  case i: Int => "Int " + i
  case l: List[_] => "List of length " + l.size
  case _: BigInt => "BigInt" 
  case x => "Something else: " + x
}
//--> test: (Any)java.lang.String

test("hallo")
//--> res4: java.lang.String = String hallo
test(42)
//--> res5: java.lang.String = Int 42
test(List(2,3,4))
//--> res6: java.lang.String = List of length 3
println(test(BigInt(123)))
//--> BigInt
println(test(42L))
//--> Something else: 42

Hier lauert allerdings schon die erste kleinere Falle: Dies ist meines Wissens die einzige Stelle in Scala, bei der Groß- und Kleinschreibung eine Rolle spielt: Obiges Schema funktioniert nur, wenn die zu belegenden Variablen (also hier s, i, l und x) klein geschrieben werden.

Kann man in den Mustern eigentlich auch auf bereits definierte Variablen im Sichtbarkeitsbereich zugreifen? Man kann, muss diese aber in Backticks einschließen. Dieses eher selten gebrauchten Zeichen befinden sich auf einer deutschen Tastatur zwischen ß und Backspace, man muss Shift und diese Taste drücken, und danach die Leertaste. Ein etwas an den Haaren herbeigezogenes Beispiel:

def test(x: String, y: String, value: String) = {
  val xy = x + y
  val yx = y + x 
  value match {
    case `xy` => "XY"
    case `yx` => "YX"
    case _ => "nothing"
  }
}
//--> test: (String,String,String)java.lang.String

test("der", "Spargel", "Spargelder")
//--> res8: java.lang.String = YX

Ein weitere nützliche Hilfe sind Guards (oder „Wächter“), mit denen man zusätzliche Bedingungen an einen bestimmten Fall stellen kann. Diese werden mit if eingeleitet, man braucht aber keine Klammern wie beim normalen if:

def test(value: Int) = value match {
  case 2 | 3 | 5 => "Primzahl"
  case i if i%2==0 || i%3==0 || i%5==0 => "zusammengesetzte Zahl"
  case _ => "da muss ich genauer ueberlegen"
}
//--> test: (Int)java.lang.String

println(test(135))
//--> zusammengesetzte Zahl
println(test(121))
//--> da muss ich genauer ueberlegen

Nun kommen wir zum Patternmatching mit Fallklassen. Ist eine Klasse als Fallklasse definiert worden, kann man sie direkt als Muster verwenden und dabei Variablen für ihre Einzelteile vergeben. Typische Beispiele dafür sind Tupel und die Unterklassen von List (:: und Nil), Option (Some und None) und Either (Left und Right). Sogar geschachtelte Kombinationen sind erlaubt:

def test(list: List[Option[String]]) = list match {
  case Some(x) :: Some(y) :: _ => x + y
  case Some(x) :: None :: _ => x 
  case None :: Some(y) :: _ => y
  case head :: Nil => "List with one element: " + head
  case Nil => "empty list"
}
//--> test: (List[Option[String]])java.lang.String

println(test(List(Some("a"),Some("b"))))
//--> ab
println(test(List(None,Some("c"))))
//--> c
println(test(List(Some("x"))))
//--> List with one element: Some(x)
println(test(List()))
//--> empty list

Das Verhalten von Fallklassen in Mustern ist nichts magisches. In gewöhnlichen Klassen kann man den gleichen Effekt erreichen, indem man eine geeignete unapply()-Methode implementiert. Gottseidank brauche ich das nicht auch noch aufzuschreiben, denn das habe ich schon einmal erklärt.

Hat eine Fallklasse eine variable Anzahl Parameter, kann man „den Rest“ mit _* überspringen, denn _ würde sich ja nur auf ein einzelnes Element beziehen. Man beachte, dass dieser Rest auch „leer“ sein kann:

 
def test(list: List[String]) = list match {
  case List("1","2", _*) => "one, two ..."
  case _ => "dunno"
}
//-->test: (List[String])java.lang.String

println(test(List("1","2")))
//--> one, two ...
println(test(List("1","2","3","4")))
//--> one, two ...

Ein Feature, das ich selbst noch nicht benutzt habe, ist das „Variable Binding“. Damit kann man bei geschachtelten Fallklassen einen Teilausdruck sowohl an eine Varible binden, als auch gleichzeitig weiter zerlegen (eventuell mit weiteren Variablen-Bindungen). Klingt komplizierter als es ist. Im folgenden Beispiel suchen wir in einer Liste von Listen eine, deren erste Unterliste als erstes Element ein „x“ enthält, und wollen sowohl die gesamte Unterliste wie auch deren zweites Element jeweils an eine Variable gebunden haben. Dazu schreibt man zuerst die Variable für den gesamten Teilausdruck, und hinter einem @ die weitere Zerlegung entsprechend einem vorgegebenen Muster:

def test(list: List[List[String]]) = list match {
  case (sublist @ List("x", second, _*)) :: _ => "" + sublist + " | " + second
  case _ => "nothing"
}
//--> test: (List[List[String]])java.lang.String

println(test(List(List("x","y","z"))))
//--> List(x, y, z) | y
println(test(List(List("w","x","y","z"))))
//--> nothing

Natürlich lassen sich alle hier vorgestellten Möglichkeiten beliebig miteinander kombinieren. Aber nur weil man es kann heißt das noch lange nicht, dass man es auch muss. Patternmatching ist ein tolles Werkzeug – wer beispielsweise jemals in Java das Visitor Pattern implementieren musste, wird es sicher zu schätzen wissen. Ich rate aber aus eigener Erfahrung dazu, ein wenig Augenmaß zu bewahren, denn wie heißt es so schön: „Wenn das einzige Werkzeug, was man hat, ein Hammer ist, sieht jedes Problem aus wie ein Nagel.“

Auch wenn ich nicht ins Detail gegangen bin, hoffe ich, einen einigermaßen vollständigen und verständlichen Überblick über dieses wichtige Teilgebiet der Scala-Syntax gegeben zu haben.

Advertisements

3 Gedanken zu “Musterlösung Patternmatching

  1. Ein wirklich toller Artikel, in dem ich sehen kann, dass ich noch viel über Scala lernen muss :). Ich werde jetzt öfters auf diesen Blog reingucken und bei Unklarheiten meinen Senf dazugeben.

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