Doppel-Switch in Java

Ich hasse es, wenn ich geschachtelte switch-Blöcke sehe. Es ist einfach unübersichtlich, und während man Fälle zusammenfassen kann, wo einem der zweite, „innere“ Wert egal ist, geht das für den ersten, „äußeren“ Wert nicht.

Nehmen wir als Anwendungsfall einmal ein boolesches TriState-Enum, das auch „und“- und „oder“-Operationen unterstützt. Eine mögliche Implementierung könnte so aussehen:

public enum TriState {
    TRUE, FALSE, UNKNOWN;

    private static TriState and(TriState a, TriState b) {
        switch(a) {
            case FALSE: return FALSE;
            case TRUE: return b;
            case UNKNOWN: switch(b) {
                case FALSE: return FALSE;
                default: return UNKNOWN;
            }
        }
        throw new AssertionError();
    }

    private static TriState or(TriState a, TriState b) {
        switch(a) {
            case TRUE: return TRUE;
            case FALSE: return b;
            case UNKNOWN: switch(b) {
                case TRUE: return TRUE;
                default: return UNKNOWN;
            }
        }
        throw new AssertionError();
    }
}

Man kann sich leicht vorstellen, wie bei mehr Werten die switches schnell unübersichtlich werden. Hier ein Beispiel, wie es mit DSL aussehen könnte:

public enum TriState {TRUE, FALSE, UNKNOWN;

    private static TriState and(TriState a, TriState b) {
        return switch2(a, b,
                case2(TRUE, TRUE, () -> TRUE),
                case2(FALSE, any(), () -> FALSE),
                case2(any(), FALSE, () -> FALSE),
                default2(() -> UNKNOWN)
        );
    }

    private static TriState or(TriState a, TriState b) {
            return switch2(a, b,
                    case2(FALSE, FALSE, () -> FALSE),
                    case2(TRUE, any(), () -> TRUE),
                    case2(any(), TRUE, () -> TRUE),
                    default2(() -> UNKNOWN)
            );
    }
}

Ich habe mich entschlossen, die „Leerstellen“ über einen speziellen Wert any() zu kennzeichnen. Wenn beide Werte egal sind, habe ich analog zum normalen switch als Synonym auch die Methode default2 bereitgestellt. Ich hoffe, dass das die Bedienung intuitiver macht. Hier die Implementierung des DSLs:

import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Supplier;

public final class Switch {
    private Switch(){ /*do not instantiate*/ }

    enum Any {any}

    public static Any any() {
        return Any.any;
    }

    @SafeVarargs
    public static <A,B,R> R switch2(A a, B b, BiFunction<A, B, Optional<R>>... cases) {
        return Arrays.stream(cases)
            .map(biFunction -> biFunction.apply(a,b))
            .flatMap(opt -> opt.map(Stream::of).orElseGet(Stream::empty))
            .findFirst()
            .orElseThrow(() -> new RuntimeException("no match found"));
    }

    public static <A,B,R> BiFunction<A, B, Optional<R>> case2(A a, B b, Supplier<R> result) {
        return (_a, _b) -> a.equals(_a) && b.equals(_b) ? Optional.of(result.get()) : Optional.empty();
    }

    public static <A,B,R> BiFunction<A, B, Optional<R>> case2(A a, Any any, Supplier<R> result) {
        return (_a, _b) -> a.equals(_a) ? Optional.of(result.get()) : Optional.empty();
    }

    public static <A,B,R> BiFunction<A, B, Optional<R>> case2(Any any, B b, Supplier<R> result) {
        return (_a, _b) -> b.equals(_b) ? Optional.of(result.get()) : Optional.empty();
    }

    public static <A,B,R> BiFunction<A, B, Optional<R>> case2(Any any1, Any any2, Supplier<R> result) {
        return default2(result);
    }

    public static <A,B,R> BiFunction<A, B, Optional<R>> default2(Supplier<R> result) {
        return (_a, _b) -> Optional.of(result.get());
    }
}

Auch wenn man in anderen Sprachen noch viel „natürlichere“ DSLs schreiben kann, ist es doch immer wieder schön zu sehen, wie „erweiterbar“ Java inzwischen geworden ist, und das ohne größeren Aufwand. Ich glaube aber, dass bei vielen Java-Entwicklern das Bewusstsein fehlt, welche Möglichkeiten sie inzwischen haben. Das ist schade, aber ich hoffe sehr, dass sich das langsam ändert.

Wir bauen ein Chamäleon

Java macht es einem nicht gerade leicht, DSLs zu schreiben und dabei typsicher zu bleiben. Angenommen, wir wollen eine DSL zum Erstellen von SQL-Abfragen in der Form select(„id“).from(„person“).where(„age > 18“).and(„name like ‚Daniel %'“) basteln: Auf der einen Seite wollen wir vermeiden, einen eigenen Typ für jeden Auswertungsschritt zu erstellen, auf der anderen Seite darf natürlich nicht erlaubt sein, das and() direkt hinter dem select() oder from() aufzurufen, oder from() zweimal u.s.w. Eine Technik, die genau das erlaubt, nenne ich „das Chamäleon“: Unser Objekt bleibt immer das gleiche, schmückt sich aber mit verschiedenen Interfaces. Solange der Nutzer artig ist und nicht castet, ist damit die Typsicherheit gesichert. Und nun etwas Butter bei die Fische:

public interface SelectQuery {
  public FromQuery from(String table);
}

public interface FromQuery {
  public WhereQuery where(String condition);
}

public interface WhereQuery {
  public WhereQuery and(String condition);
  public WhereQuery or(String condition);
}

public class Query implements SelectQuery, FromQuery, WhereQuery {
  private String[] columns;
  private String table;
  private String condition;

  private Query(String... columns) {
    this.columns = columns;
  }

  public FromQuery from(String table) {
    this.table = table;
    return this;
  }

  public WhereQuery where(String condition) {
    this.condition = condition;
    return this;
  }

  public WhereQuery and(String condition) {
    this.condition += " and " + condition;
    return this;
  }

  public WhereQuery or(String condition) {
    this.condition += " or " + condition;
    return this;
  }
  
  @Override public String toString() {
    String cols = java.util.Arrays.toString(columns);
    return "SELECT " + cols.substring(1,cols.length()-1) + " FROM " + table + 
           (condition == null ? "" : " WHERE " + condition);
  }

  public static SelectQuery select(String... columns) { 
     return new Query(columns); 
  }
}

Ein kleiner Test:

public class Test {
  public static void main(String[] args) {
    System.out.println(Query.select("id","name","age").from("person").
        where("age >= 18").and("name like 'Daniel %'")
    );  
  }
}

//--> SELECT id, name, age FROM person WHERE age >= 18 and name like 'Daniel %'

Sachen wie Query.select(„id“,“name“,“age“).where(„age >= 18“).from(„person“) kompilieren nicht. Natürlich ist das hier nur ein Spiel-Beispiel, eine „ordentliche“ DSL müsste schon etwas mehr leisten: GROUP, HAVING, ORDER BY, Unterabfragen, typsichere Ausdrücke statt Strings wie „age >= 18″… Und ich denke, dass sich nicht alle diese Kombinationen mit dem „Chamäleon“ lösen lassen. Trotzdem ist es ein nützliches Werkzeug, um Java ein bisschen gemütlicher einzurichten.

In Scala hat man es natürlich leichter, z.B. gibt es dort den SQL-Wrapper DBC, dessen neueste Version jedoch leider auf sich warten lässt. Aber schick aussehen tut es jedenfalls, wie man hier sehen kann.

Marskratervermeidung

Wie wir wissen hat die NASA einen kleinen, aber teuren Marskrater erzeugt, nur damit die Schulkinder auf der Erde ein schönes Beispiel dafür haben, wie wichtig es ist, bei einer Berechnungen penibel auf die richtigen physikalischen Einheiten zu achten.

Wenn man sich die Antwort auf dieses Problem in Java-Land anschaut (nämlich JSR-275), kommt sicher ein bisschen Bewunderung angesichts des freigiebigen und virtuosen Gebrauchs von Generics auf, lässt aber die Frage nach der Praktikabilität einer solchen Lösung offen. Nun ja, vielleicht schafft ja Project Coin Linderung

Hier nun eine kleine Scala-Spielerei, die zeigt, wie leichtgewichtig man ein kleines Einheiten-Framework implementieren und wie schmerzlos dabei die Syntax gestaltet werden kann.

case class Length(mm:Int) {
  def +(that:Length) = Length(this.mm + that.mm)
  def -(that:Length) = Length(this.mm - that.mm)
  def *(scalar:Int) = Length(scalar * mm)
}

So weit, so gut. Unsere Längen basieren intern auf mm, aber natürlich kann man ohne weiteres auch Methoden zum Abfragen der cm, m oder km schreiben. Addieren und Subtrahieren kann die Klasse auch. Im richtigen Leben würde man statt Int wahrscheinlich Double oder sogar BigDecimal nehmen, je nach Anwendung.

implicit def intToMM(value:Int) = new { 
    def mm = Length(value) 
}
implicit def intToCM(value:Int) = new {  
    def cm = Length(value*10) 
}
implicit def intToM(value:Int) = new {  
    def m = Length(value*1000) 
}
implicit def intToKM(value:Int) = new {  
    def km = Length(value*1000000) 
}

Die impliziten Konvertierungen verwenden anonyme Klassen – wozu sollen die temporären Objekte auch einen Namen bekommen, wenn sie nur dazu da sind, uns die passenden Length-Objekte zu liefern?

Ein kleiner Test:

(3 m) + (5 cm) == (3050 mm)
//--> res0: Boolean = true
(3 m) + (5 cm) == (3005 mm)
//--> res1: Boolean = false

Tja, die Operatorpräzedenz ist festgelegt, da hilft auch kein Flehen und Brummen, wir brauchen hier leider die Klammern. Aber trotzdem finde ich diese Version viiieeel lesbarer als Java. Auf jeden Fall ist die hier gezeigte Technik es ein einfaches, aber nützliches Werkzeug bei der Gestaltung eigener DSLs.

Typberatung à la Scala

Heute will ich die Frage behandeln: Was tun, wenn der Typ nicht passt? Scala hat darauf viele Antworten, unter anderem strukturelle Typen und implizite Konvertierungen.

Strukturelle Typen – typsicheres Ducktyping

Ein lästiges Beispiel aus meiner beruflichen Praxis sind StringBuilder und StringBuffer. Beide Klassen haben fast identische Methoden, und beide Klassen implementieren Appendable und CharSequence. Das Problem ist, dass diese beiden Interfaces ziemlich spartanisch daherkommen und sich untereinander nicht viel zu sagen haben:

 
public interface CharSequence {
    int length();
    char charAt(int index);
    CharSequence subSequence(int start, int end);
    public String toString();
} 

public interface Appendable {
    Appendable append(CharSequence csq) throws IOException;
    Appendable append(CharSequence csq, int start, int end) throws IOException;
    Appendable append(char c) throws IOException;
} 

Toll, und wie setze ich jetzt mit einer einzigen Methode z.B. einen StringBuilder/Buffer durch setLength(0) zurück? Es gibt immer noch Tonnen von Code mit dem „alten“ StringBuffer, und da StringBuilder nicht threadsicher ist, ist „Suchen und Ersetzen“ eine gefährliche Lösung. Wäre es nicht schön, einfach sagen zu können: „Mir ist egal, welcher Typ da ankommt, Hauptsache er hat die und die Methoden“? Genau das ist ein struktureller Typ:

def clear(sb:{def setLength(length:Int)}) {
  sb.setLength(0)
}

val builder = new StringBuilder("hallo")
clear(builder)
println(builder.toString == "")
//--> true

Viel mehr gibt es dazu nicht zu sagen: Der strukturelle Typ {def setLength(length:Int)} funktionert so ziemlich überall, wo es ein normaler Typ tun würde. Soweit ich weiß, kann man von einem struktuellen Typ keine anderen Typen ableiten, aber das ist eigentlich auch logisch.

Skriptsprachen lösen das gleiche Problem mit Duck-Typing („if it walks like a duck…“), allerdings mit dem kleinen Unterschied, dass einem der Code zur Laufzeit um die Ohren fliegt, wenn das übergebene Objekt die entsprechende Methode nicht besitzt.

Implizite Konvertierungen

… sind eine mächtige Waffe, mit der mach sich auch mächtig in den Fuß schießen kann. Sagt nicht, ich hätte euch nicht gewarnt! Aber diese Waffe macht erst Dinge wie DSLs und das „Pimp my Library“-Pattern möglich. Ich möchte hier eine etwas ungewöhnliche Anwendung zeigen, nämlich „disjunkte Typen“ (eine Variable kann Werte verschiedener Typen besitzen, die nichts miteinander zutun haben). Zuerst das Grundgerüst ohne implizite Umwandlungen:

sealed abstract class |[P,Q] //die Klasse heißt wirklich | 
case class LeftOr[P,Q](value:P) extends |[P,Q]
case class RightOr[P,Q](value:Q) extends |[P,Q]

def doubleMe(t: Int | String) {
    t match {
        case LeftOr(i) => println(2*i)
        case RightOr(s) => println(s + " " + s)
    }
}

doubleMe(LeftOr(42))
//--> 84
doubleMe(RightOr("Bora"))
//--> Bora Bora

Ähnlich wie bei Methoden ist auch bei Typen die Infix-Schreibwiese erlaubt, was uns das hübsche Int | String anstatt |[Int,String] erlaubt. Weniger schön ist der Aufruf, bei dem wir selbst einen der Untertypen instantiieren müssen – und auch noch aufpassen, dass wir den richtigen erwischen, denn hier geht nur LeftOr(Int) oder RightOr(String). Nun fügen wir implizite Konvertierungen hinzu, und zwar nicht nur für unseren speziellen Fall, sondern gleich generisch für alle möglichen „Belegungen“ unserer Typen.

implicit def leftToDis[P,Q](left:P) = LeftOr[P,Q](left)
implicit def rightToDis[P,Q](right:Q) = RightOr[P,Q](right)

doubleMe(23)
//--> 46
doubleMe("Cha")
//--> Cha Cha

Viel besser, nicht wahr? So funktionert es: Wenn der Compiler feststellt, dass der falsche Typ vorliegt (oder z.B. eine bestimmte Methode nicht vorhanden ist), prüft er als allerletzen Ausweg bevor er aufgibt, ob es eine implizite Umwandlung gibt. Diese Umwandlung muss eindeutig sein, und muss die Typen direkt ineinander konvertieren (Ketten von impliziten Umwandungen sind also verboten). In unserem Fall erkennt der Compiler, dass der Aufruf von doubleMe mit einem der beiden Untertypen von | erfüllt werden könnte, und dann benutzt er jeweils eine der beiden impliziten Methoden zur Konvertierung.

Viele Beispiele für implizite Konvertierungen finden sich in Scala selbst, etwa in scala.Predef (diese Klasse wird beim Starten von Scala automatisch importiert). Dort findet sich u. a. folgende implizite Methode:

implicit def stringWrapper(x: String) = new runtime.RichString(x)

Die Klasse scala.runtime.RichString implementiert nun jede Menge nützliche Methoden, die Java-Strings fehlen, so dass man z.B. schreiben kann:

println("Cha " * 3)
//--> Cha Cha Cha 
println("!dlroW olleH".reverse)
//--> Hello World!

Ich hoffe, meine kleine Scala-Typberatung hat euch gefallen, auch wenn wir damit nur an der Oberfläche gekratzt haben.