Pattern-Matching in Java 8

In Scala ist Pattern-Matching ein beliebtes Feature mit fast unbegrenzten Anwendungsmöglichkeiten. Nicht umsonst wird es oft als „switch auf Steroiden“ bezeichnet. Im Rahmen eines kleinen Projekts habe ich überlegt, ob man das nicht auch in Java 8 nachbauen kann.

Man kann, mit einigen Einschränkungen. Hier einmal ein kleines Anwendungsbeispiel:

String string = ...
String s = match(string,
   StartsWith("foo", () -> "found foo..."),
   EndsWith("bar", () -> "found ...bar"),
   Contains("baz", () -> "found ...baz..."),
   EqualsIgnoreCase("blubb", () -> "found blubb"),
   Default(() -> "nix gefunden")
);
System.out.println(s);

Wie funktioniert das? Zuerst braucht man für die einzelnen Fälle ein Interface:

import java.util.Optional;

public interface Case<T,R> {
    Optional<R> accept(T value);
}

Es ist im Prinzip eine Art Funktion, die für einem Ausgangswert ein Resultat liefert, allerdings in einem Optional. Wir haben also eine Art Funktion, die eventuell etwas zurückliefert – genau dann, wenn der zu überprüfende Wert „passt“. Hier eine der verwendeten Case-Implementierungen (man verzeihe mir bitte die an Scala angelehnte Großschreibung der Methodennamen):

public static <R> Case<String, R> StartsWith(String prefix, Supplier<R> supplier) {
    return t -> t.startsWith(prefix)
            ? Optional.of(supplier.get())
            : Optional.<R>empty();
}

Wir liefern also den durch den Supplier vorgegebenen Wert (in ein Optional verpackt) zurück, aber nur, wenn der zu testende String den richtigen Präfix hat.

Die match-Methode ist auch recht unspektakulär (MatchException ist eine gewöhnliche Laufzeit-Exception):

@SafeVarargs
public static <T,R> R match(T value, Case<T,R> ... cases) throws MatchException {
    for(Case<T,R> c : cases) {
        Optional<R> result = c.accept(value);
        if (result.isPresent()) {
            return result.get();
        }
    }
    throw new MatchException();
}

Interessanterweise kann dieser Mechanismus auch die Arbeit mit Optional vereinfachen. Wenn wir einmal so tun, als hätte Optional wie in Scala die Unterklassen Some (die einen Wert enthält) und None (die „leer“ ist), könnte uns das zu dieser Verwendung anregen:

Optional<Integer> opt = ...
String s = match(opt,
        None(() -> "leer"),
        Some(42, () -> "die Antwort!"),
        SomeIf(a -> "gerade", a -> a % 2 == 0),
        Default(() -> "nix gefunden")
);

Selbst „geschachteltes“ Pattern-Matching ist möglich:

Optional<Optional<Integer>> opt = ...
String s = match(opt,
        None(() -> "leer"),
        Some(None(() -> "leer eingetütet")),
        Some(SomeIf(a -> "gerade", a -> a % 2 == 0)),
        Default(() -> "nix gefunden")
);

Hier die verwendeten Case-Implementierungen:

import static java.util.Optional.*;
...

public static <T,R> Case<Optional<T>,R> None(Supplier<? extends R> supplier) {
    return t -> t.isPresent()
            ? Optional.<R>empty()
            : of(supplier.get());
}

public static <T,R> Case<Optional<T>,R> Some(T value, Supplier<? extends R> supplier) {
    return t -> t.isPresent() && t.get().equals(value)
            ? of(supplier.get())
             : Optional.<R>empty();
}

public static <T,R> Case<Optional<T>,R> Some(Case<T,R> caseT) {
    return t -> t.isPresent()
            ? caseT.accept(t.get())
            : Optional.<R>empty();
}

public static <T,R> Case<Optional<T>,R> Some(Function<? super T, ? extends R> fn) {
    return t -> t.isPresent()
            ? of(fn.apply(t.get()))
            : Optional.<R>empty();
}

public static <T,R> Case<Optional<T>,R> SomeIf(Function<? super T, ? extends R> fn, Predicate<T> predicate) {
    return t -> t.isPresent() && predicate.test(t.get())
            ? of(fn.apply(t.get()))
            : Optional.<R>empty();
}

public static <T,R> Case<T,R> Default(Supplier<R> supplier) {
    return value -> of(supplier.get());
}

Ich will nicht verhehlen, dass wir hier haarscharf an den Grenzen von Javas Typ-Inferenz operieren, und auch leicht Eindeutigkeitsprobleme bei gleichnamigen statischen Methoden mit Lambda-Argumenten auftreten können. Ob diese kleine DSL wirklich „brauchbar“ ist, muss sich erst noch zeigen. Spaß macht es auf jeden Fall.

Werbeanzeigen

Multiples Pattern-Matching

Auf Stackoverflow habe ich schon häufiger die Frage gelesen, wie man es anstellt, dass beim Pattern-Matching nicht nur der erste passende Fall berücksichtigt werden, sondern alle passenden Fälle. Eingebaut gibt es das leider nicht, es ist aber auch nicht schwer zu realisieren. Hier eine recht elegante Version von Philippe:

class MatchAll[S](scrutinee : =>S) {
  def matchAll[R](patterns : PartialFunction[S,R]*) : Seq[R] = {
    val evald : S = scrutinee
    patterns.flatMap(_.lift(evald))
  }
}

implicit def anyToMatchAll[S](scrut : =>S) : MatchAll[S] = new MatchAll[S](scrut)

Und die Anwendung:

(2,4) matchAll (
  { case (2,_) => "starts with two" },
  { case (3,_) => "starts with three" },
  { case (_,4) => "ends with four" },
  { case (a,b) if a + b == 6 => "adds up to six" }
)

//--> ArrayBuffer(starts with two, ends with four, adds up to six)

Ich denke, dass man mit der im Vergleich zu match leicht veränderten Syntax gut leben kann, und fände es gut, wenn dieses Konstrukt auch in die Scala-Bibliotheken aufgenommen würde.

Resteverwertung

Als Ergänzung zu meinem Pattern-Matching-Artikel heute ein kleiner Trick, der mir eher zufällig über den Weg gelaufen ist.

Ich hatte dazumal folgendes Codebeispiel gebracht, um zu zeigen, wie man die restlichen Elemente einer Collection beim Pattern Matching erfolgreich ignorieren kann:

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

Jetzt fragt man sich natürlich, wie man vorgehen muss, wenn man den Rest eben nicht ignorieren will, sondern auch diesen an eine Variable gebunden haben möchte. Die „naive“ Lösung mit etwas wie rest* funktioniert jedenfalls schon einmal nicht. Aber es geht trotzdem, und zwar dank Klammeraffen:

def test(list: List[String]) = list match {
  case List("1","2", rest @ _*) => "one, two with rest " + rest
  case _ => "dunno"
}

test(List("1","2","3","4"))
//--> java.lang.String = one, two with rest List(3, 4)

Achtung, der Rest kann natürlich auch eine leere Liste sein. Für Listen ist dieser Trick eigentlich unnötig, denn hier hat man ja schon den Extraktor ::, aber das Ganze funktioniert natürlich auch für andere Extraktoren mit variabler Argumentanzahl, wie z.B. Seq.

So, dann wünsche ich fröhliches Extrahieren, vor allem meinen Erstlesern bei codekicker.de.

Eigene Extraktoren

Heute nur ein paar Zeilen zu eigenen Extraktoren. Ich denke, einen guten Überblick über die verschiedenen Möglichkeiten gibt Jesse Eichar in seinem Daily Scala Blog. Ich will heute einen ganz einfachen Satz von Extraktoren vorstellen, mit denen man sich in vielen Fällen Guards sparen kann. Angenommen, wir haben folgenden Code:

case class Edge(from:String, to:String)

def makeEdge(p:(String,String)) = p match {
  case (from,to) if from != to => Edge(from, to)
  case _ => error("Can't create edge with identical end points")
}

Wäre es nicht schön, wenn wir einen Extraktor hätten, der auf Ungleichheit prüfen kann? Kein Problem:

object Ne {
   def unapply[T](pair:(T,T)):Option[(T,T)] = 
     if (pair._1 != pair._2) Some(pair) else None
}

def makeEdge(p:(String,String)) = p match {
  case Ne(from,to) => Edge(from, to)
  case _ => error("Can't create edge with identical end points")
}

Nach dem gleichen Schema lässt sich eine ganze Familie von Extraktoren basteln. Für Größenvergleiche benötigen wir zusätzlich einen impliziten Ordering-Parameter, und für den Gleichheits-Extraktor geben wir natürlich nur einen Wert zurück – zwei gleiche wären ja ziemlich witzlos. Hier die ganze Sammlung:

object Eq {
   def unapply[T](pair:(T,T)):Option[T] = 
      if (pair._1 == pair._2) Some(pair._1) else None
}

object Ne {
   def unapply[T](pair:(T,T)):Option[(T,T)] = 
     if (pair._1 != pair._2) Some(pair) else None
}

object Lt {
   def unapply[T](pair:(T,T))(implicit ord: Ordering[T]):Option[(T,T)] = 
     if (ord.lt(pair._1,pair._2)) Some(pair) else None
}

object Le {
   def unapply[T](pair:(T,T))(implicit ord: Ordering[T]):Option[(T,T)] = 
     if (ord.lteq(pair._1,pair._2)) Some(pair) else None
}

object Gt {
   def unapply[T](pair:(T,T))(implicit ord: Ordering[T]):Option[(T,T)] = 
     if (ord.gt(pair._1,pair._2)) Some(pair) else None
}

object Ge {
   def unapply[T](pair:(T,T))(implicit ord: Ordering[T]):Option[(T,T)] = 
     if (ord.gteq(pair._1,pair._2)) Some(pair) else None
}

Für eine Beschränkung habe ich leider noch keine Lösung gefunden, und zwar für Alternativen. Man kann zwar schreiben:

def g(p:(Int,Int)) = p match {
  case (10,20) | (20,10) => println("yes!")
  case _ => println("nope")
}

Aber es ist verboten, in den einzelnen Alternativen Variablen zu belegen, z.B.:

//doesn't work
def g(p:(Int,Int)) = p match {
  case (10,n) | (n,10) => println(n)
  case _ => println("nope")
}

Im Prinzip sollte das für den Compiler kein Problem sein, wenn in beiden Zweigen die gleiche Anzahl Variablen mit den gleichen Typen verwendet wird. Leider scheint es auch mit eigenen Extraktoren nicht möglich zu sein, das gewünschte Verhalten abzubilden. Oder hat jemand vielleicht eine zündende Idee?

So, ich denke das reicht für heute. Demnächst gibt es das Kohl-Ziege-Wolf-Puzzle in Haskell, aber das braucht noch etwas Feinschliff, vor allem der Kohl.

Listen-Urschleim: 99 Scala-Probleme

Hatte ich eigentlich schon die schöne Aufgabensammlung 99 Scala-Probleme erwähnt? Dann hole ich das hiermit nach und lege sie allen Scala-Adepten ans Herz.

Die ersten 28 Aufgaben beschäftigen sich mit Listen. Inzwischen habe ich genug Scala-Erfahrung, um das ziemlich problemlos hinzubekommen. Damit es trotzdem eine kleine Herausforderung wird, habe ich mir noch zwei nette Zusatzbedinungen ausgedacht:

  1. Ich verwende meinen eigenen, wirklich strunzdoofen Listentyp
  2. Alle meine Lösungen müssen endrekursiv sein

Den einzigen kleinen Luxus, den mein Listentyp bietet, ist die apply()-Methode zum bequemen Initialisieren:

sealed trait LIST[+T]

case class CONS[T](car: T, cdr: LIST[T]) extends LIST[T]

case object NIL extends LIST[Nothing] 

object LIST {
  def apply[T](seq:T*):LIST[T] =
    if (seq.isEmpty) NIL else CONS(seq.head, LIST(seq.tail:_*))
}

Die ersten drei Methoden sind leicht:

import scala.annotation.tailrec

object NinetyNine {

  //P01 (*) Find the last element of a list
  @tailrec
  def last[T](list: LIST[T]): T = list match {
    case CONS(x, NIL) => x
    case CONS(_, xs) => last(xs)
    case NIL => error("last on empty LIST")
  }

  //P02 (*) Find the last but one element of a list.
  @tailrec
  def penultimate[T](list: LIST[T]): T = list match {
    case CONS(x, CONS(_ , NIL)) => x
    case CONS(_, xs) => penultimate(xs)
    case _ => error("not enough elements in LIST")
  }

  //P03 (*) Find the Kth element of a list.
  @tailrec
  def nth[T](n: Int, list:LIST[T]): T = list match {
    case CONS(x, _) if n == 0 => x
    case CONS(_, xs) if n > 0 => nth(n-1, xs)
    case _ => error("not enough elements in LIST or negative index")
  }
...

Methode Nummer 4 und 5 könnte man auch in diesem Stil schreiben, aber dann sind sie nicht mehr endrekursiv. Die Standardlösung für solchen Fälle ist eine lokale Methode mit (meist) einem zusätzlichen Argument, dem „Akkumulator“, der den Zwischenstand des Endergebnisses speichert. Die Palindrom-Methode Nummer 6 bedient sich frech bei Nummer 5, denn mir ist beim besten Willen keine effizientere Implementierung eingefallen.

...
  //P04 (*) Find the number of elements of a list.
  def length(list:LIST[_]): Int = {
    @tailrec
    def len(li:LIST[_], count: Int): Int = li match {
      case CONS(_, xs) => len(xs, count + 1)
      case NIL => count
    }
    len(list, 0)
  }

  //P05 (*) Reverse a list.
  def reverse[T](list:LIST[T]):LIST[T] = {
    @tailrec
    def rev(rest:LIST[T], accu:LIST[T]): LIST[T] = rest match {
      case NIL => accu
      case CONS(x, xs) => rev(xs, CONS(x, accu))
    }
    rev(list, NIL)
  }

  //P06 (*) Find out whether a list is a palindrome.
  def isPalindrome[T](list: LIST[T]) = list == reverse(list)
...

Methode Nummer 7 war dann schon etwas schwieriger, aber wenn man erst einmal eine Methode zum Aneinanderhängen von Listen hat, wird es leichter. Wem die Pattern-Matching-Syntax mit dem @-Klammeraffen spanisch vorkommt, findet sie am Ende dieses Artikels erklärt. Ehrlich gesagt ist mir nicht ganz klar, warum flatten trotz des zweiten case-Statements als endrekursiv durchgeht. Jedenfalls ziehe ich vor dem Scala-Compiler meinen Hut, der bei dieser Optimierung anscheinend ganze Arbeit leistet.

...
  //helper method
  def append[T](aList: LIST[T], bList: LIST[T]): LIST[T] = {
    @tailrec
    def app(aList: LIST[T], bList: LIST[T]): LIST[T] = aList match {
      case CONS(a, as) => app(as, CONS(a, bList))
      case NIL => bList
    }
    app(reverse(aList), bList)
  }  

  //P07 (**) Flatten a nested list structure.
  @tailrec
  def flatten(list: LIST[Any]): LIST[Any] = list match {
    case CONS(NIL, xs) => flatten(xs)
    case CONS(x@CONS(_,_), xs) => append(flatten(x), flatten(xs))
    case CONS(x, xs) => CONS(x, flatten(xs))
    case NIL => NIL
  }
...

Die nächsten drei Methoden beschäftigen sich mit verschiedenen Arten der „Kompression“. Wieder begegnen uns die lokalen Methoden mit ihren „Akkus“:

...
  //P08 (**) Eliminate consecutive duplicates of list elements.
  @tailrec
  def compress[T](list: LIST[T]): LIST[T] = list match {
    case NIL => NIL
    case CONS(a, as@CONS(b, _)) if a == b => compress(as)
    case CONS(a, as) => CONS(a, compress(as))
  }

  //P09 (**) Pack consecutive duplicates of list elements into sublists.
  def pack[T](list: LIST[T]): LIST[LIST[T]] = {
    @tailrec
    def pck[T](list: LIST[T], accu: LIST[LIST[T]]): LIST[LIST[T]] = (list, accu) match {
      case (NIL, _) => reverse(accu)
      case (CONS(a, as), CONS(bbs@CONS(b, bs), cs)) if a == b => pck(as, CONS(CONS(a,bbs),cs))
      case (CONS(a,as), _) => pck(as, CONS(CONS(a, NIL), accu))
    }
    pck(list, NIL)
  }

  //P10 (*) Run-length encoding of a list.
  def encode[T](list: LIST[T]): LIST[(Int,T)] = {
    @tailrec
    def rle[T](list: LIST[T], accu: LIST[(Int,T)]): LIST[(Int,T)] = (list, accu) match {
      case (NIL, _) => reverse(accu)
      case (CONS(a, as), CONS((n, b), cs)) if a == b => rle(as, CONS((n+1,b), cs))
      case (CONS(a,as), _) => rle(as, CONS((1, a), accu))
    }
    rle(list, NIL)
  }
...

Ich denke, das ist genug für heute. Natürlich sollte man das ganze auch noch testen:

...
  def main(args:Array[String]) {
    require(last(LIST(1, 1, 2, 3, 5, 8)) == 8, "last")
    require(penultimate(LIST(1, 1, 2, 3, 5, 8)) == 5, "penultimate")
    require(nth(2, LIST(1, 1, 2, 3, 5, 8)) == 2, "nth")
    require(length(LIST(1, 1, 2, 3, 5, 8)) == 6, "length")
    require(reverse(LIST(1, 1, 2, 3, 5, 8)) == LIST(8, 5, 3, 2, 1, 1), "reverse")
    require(isPalindrome(LIST(1, 2, 3, 2, 1)), "isPalindrome")
    require(flatten(LIST(LIST(1, 1), 2, LIST(3, LIST(5, 8)))) ==
            LIST(1, 1, 2, 3, 5, 8), "flatten")
    require(compress(LIST('a, 'a, 'a, 'a, 'b, 'c, 'c, 'a, 'a, 'd, 'e, 'e, 'e, 'e))==
            LIST('a, 'b, 'c, 'a, 'd, 'e), "compress")
    require(pack(LIST('a, 'a, 'a, 'a, 'b, 'c, 'c, 'a, 'a, 'd, 'e, 'e, 'e, 'e)) ==
            LIST(LIST('a, 'a, 'a, 'a), LIST('b), LIST('c, 'c), LIST('a, 'a),
            LIST('d), LIST('e, 'e, 'e, 'e)), "pack")
    require(encode(LIST('a, 'a, 'a, 'a, 'b, 'c, 'c, 'a, 'a, 'd, 'e, 'e, 'e, 'e)) ==
            LIST((4,'a), (1,'b), (2,'c), (2,'a), (1,'d), (4,'e)), "encode")
    println("SUCCESS!!!")
  }
}

Dass man mit so rudimentären Klassen trotzdem relativ gut zurecht kommt (auch wenn es manchmal etwas langatmig wird), zeigt eindrucksvoll die Power, die hinter dem Pattern-Matching und Fallklassen steckt. Natürlich könnte man stattdessen auch einen kleinen Satz Funktionen höherer Ordnung definieren (map, foldLeft und foldRight würden sich hier anbieten) und damit weiterarbeiten.

Ich hoffe, ich habe ein wenig den Ehrgeiz geweckt, euch selbst einmal an den Aufgaben zu versuchen (egal ob nun mit List oder LIST), und vielleicht konntet ihr euch ja auch den einen oder anderen Trick abschauen.

Wie immer sind Verbesserungsvorschläge willkommen.

Alternativen

In manchen Fällen braucht man Alternativen, in diesem hier zu Option und zu Either. Denn damit ist es nur umständlich auzudrücken, dass von zwei Werten mindestestens einer vorhanden sein muss, z.B. wenn wenigstens Vor- oder Nachname eingegeben werden muss, aber auch beides erlaubt ist. Ein anderes Beispiel wäre, wenn man von einer Funktion entweder ein Resultat oder einen Fehler (bis hierhin wäre das ein typischer Fall für Either) oder aber sowohl ein Resultat wie auch eine Warnung zurückbekommen kann.

Soetwas mit den vorhandenen Mitteln auszudrücken ist nicht ganz einfach: Ein Tupel von zwei Options erlaubt auch die „verbotene“ (None, None)-Kombination, und da Either nicht beide Werte gleichzeitig abbilden kann, landet man damit bei Konstruktionen wie Either[Either[A,B], Tuple2[A,B]], mit dem kein Mensch vernünftig arbeiten kann. Dabei ist ein selbstgestrickter Alternative-Type nicht schwer zu schreiben:

sealed trait Alternative[+A,+B] {
  def leftOption: Option[A] = this match {
    case First(a) => Some(a)
    case Second(_) => None
    case Both(a, _) => Some(a)
  }

  def rightOption: Option[B] = this match {
    case First(_) => None
    case Second(b) => Some(b)
    case Both(_, b) => Some(b)
  }

  def bothOptions= (leftOption, rightOption)

  //more efficient implementation than using mapLeft and then mapRight
  def map[AA,BB](fa: A => AA, fb: B => BB):Alternative[AA,BB] = this match {
    case First(a) => First(fa(a))
    case Second(b) => Second(fb(b))
    case Both(a, b) => Both(fa(a),fb(b))
  }

  def mapLeft[AA] (fa: A => AA):Alternative[AA, B] = this match {
    case First(a) => First(fa(a))
    case Second(b) => Second(b)
    case Both(a, b) => Both(fa(a),b)
  }

  def mapRight[BB] (fb: B => BB): Alternative[A, BB] = this match {
    case First(a) => First(a)
    case Second(b) => Second(fb(b))
    case Both(a, b) => Both(a,fb(b))
  }

  def orElse[AA >: A, BB >: B](that:Alternative[AA,BB]) = (this,that) match {
    case (First(a),Second(b)) => Both(a,b)
    case (First(a),Both(_,b)) => Both(a,b)
    case (Second(b),First(a)) => Both(a,b)
    case (Second(b),Both(a,_)) => Both(a,b)
    case _ => this
  }

  def leftOrElse[AA >: A](aDefault:AA) = leftOption.getOrElse(aDefault)
  def rightOrElse[BB >: B](bDefault:BB) = rightOption.getOrElse(bDefault)
  def getOrElse[AA >: A, BB >: B](aDefault:AA, bDefault:BB) =
    (leftOrElse(aDefault), rightOrElse(bDefault))
}

case class First[+A,+B](_1:A) extends Alternative[A,B]
case class Second[+A,+B](_2:B) extends Alternative[A,B]
case class Both[+A,+B](_1:A, _2:B) extends Alternative[A,B]

Natürlich ist das nur eine von vielen Möglichkeiten, so einen Typ zu implementieren, und man könnte natürlich noch viele nützliche Sachen hinzufügen. Man kann in der Implementierung sehr schön sehen, wie sehr Fallklassen und Pattern Matching unser Leben erleichtern können. Die einzige kleine Herausforderung war, die Varianzen richtig hinzubekommen, aber da half ein bisschen Kiebitzen bei unseren „Vorbildern“ Option und Either.

Ich wollte mit diesem Beispiel zeigen, dass man nicht auf Krampf irgendwelche wilden Konstrukte mit Klassen wie Option oder Either verwenden sollte, wenn sie nicht wirklich zur Aufgabenstellung passen, weil es leicht ist, sich selbst eine passende Variante zu schreiben. Viel Spaß beim Ausprobieren!

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.