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.

Werbeanzeigen

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.