Strukturelles Pattern Matching

Die Idee ist, für algebraische Datentypen (Optional, Either, List, Tuple…) Pattern Matching ähnlich wie in Scala als DSL anzubieten:

   private static void testEither(Either<Integer, String> either) {
        String s = match(either,
                Left(42, () -> "die Antwort!"),
                LeftIf(a -> a + " ist gerade", a -> a % 2 == 0),
                Left(a -> "Links!"),
                Right("foo", () -> "FOO"),
                RightIf(a -> a + " ist lang", a -> a.length() > 10),
                Default(x -> "nix gefunden: " + x.toString())
        );
        System.out.println(s);
    }

Dabei sind in gewissem Umfang auch geschachtelte Strukturen möglich:

    private static void testOptEither(Optional<Either<Integer,String>> opt) {
        String s = match(opt,
                None(() -> "leer"),
                Some(Left(12, () -> "12 links!")),
                Some(RightIf(x -> x, x -> x.length() == 3)),
                Default(x -> "nix gefunden: " + x.toString())
        );
        System.out.println(s);
    }

Der gewählte Ansatz ist recht einfach strukturiert (im Gegensatz zu allgemeineren Versionen wie http://kerflyn.wordpress.com/2012/05/09/towards-pattern-matching-in-java/ ), aber meiner Meinung nach trotzdem recht nützlich, und vor allem auch clientseitig recht leicht auf “eigene” Klassen erweiterbar. Die einzelnen “Fälle” müssen dazu nur ein recht einfaches Interface implementieren:

import java.util.Optional;

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

Eine Implementierung sieht dann z.B. so aus:

    public static <A, B, R> Case<Either<A, B>, R> LeftIf(Function<A, R> fn, Predicate<A> predicate) {
        return t -> t.isLeft() && predicate.test(t.getLeft())
                ? of(fn.apply(t.getLeft()))
                : Optional.<R>empty();
    }

Die von mir gewählte Großschreibung ist eine bewußte Abweichung von Java-Standard:
[ul]
[li] Ähnlichkeit zu Scalas Pattern-Matching
[/li][li] Ähnlichkeit zu einem “Konstruktor” (also sozusagen ein “Destruktor”)
[/li][li] Beugt Name-Clashes mit anden Methoden (wie Either.left()) oder Schlüsselwörtern (Default) vor
[/li][/ul]

Bei solchen DSLs ist natürlich die Typinferenz immer recht diffizil (meine IDE meint auch öfters, dass bestimmte Konstrukte falsch wären, während es Java 8 problemlos schluckt). Insbesondere besteht potentiell die Gefahr, dass Konstrukte akzeptiert werden, die eigentlich nicht matchen sollten, was natürlich entsprechend getestet werden sollte.

Gibt es Meinungen dazu?

Ehrlich: Ist mir im Moment spontan erstmal zu hoch, aber werde den verlinkten Artikel nochmal lesen und vielleicht ein paar use-cases durchprobieren.

Wie gesagt ist der Ansatz im Artikel sehr allgemein und die Implementierung kompliziert. Was ich vorhabe, ist nur ein kleiner Ausschnitt daraus, nämlich Typen wie Optional, Either oder List auf ihre Bestandteile zu matchen, und das scheint mit gewissen Einschränkungen relativ einfach zu gehen.

Um vielleicht mal ein etwas eingängiges Beispiel zu geben: Man könnte z.B. einen Case schreiben, der auf String-Anfänge matched.

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

Dann könnte man so benutzen:

String s = "fool"
Integer i = match(s,
   StartsWith("foo", () -> 1),
   StartsWith("bar", () -> 2),
   StartsWith("baz", () -> 3),
   Default(() -> -1)
}

Für unsere Datentypen wäre das Ganze deshalb interessant, weil man damit viel intuitiver an die gewrappten Werte herankommt. Vergleiche:

Either<String, Integer> e = ...
String result = null;
if (e.isLeft()) {
   String content = e.getLeft();
   if (content.equals("foo") { 
       result = "bar!"
   } else {
      result = "unknown " + content;
   } 
} else {
  result = "number: " + e.getRight();
}

… und …

Either<String, Integer> e = ...
String result = match(e,
   Left("foo", () -> "bar!"),
   Left(x -> "unknown " + x),
   Right(n -> "number: " + n)
);

Der Code zum Ausprobieren steht hier: http://forum.byte-welt.net/threads/12128-Persistente-Java-Collections?p=94800&viewfull=1#post94800

*** Edit ***

… oder im Blog-Eintrag: http://forum.byte-welt.net/entries/45-Pattern-Matching-in-Java