Der Builder, der zählen konnte

In letzter Zeit habe ich mit Kotlin herumexperimentiert und bin begeistert, welche DSLs damit möglich sind. Das typsichere Builder-Pattern hatte ich hier schon öfter diskutiert, heute soll es zählen lernen. Eine Polygon-Klasse ist ein guter Kandidat für eine Beispielanwendung, denn so richtig „polygon“ ist es erst, wenn es mindestens drei Punkte hat. Starten wir also mit einer entsprechenden Klasse:

import javafx.geometry.Point2D
import javafx.scene.paint.Color

data class Polygon(val color: Color, val points: List)

Wir werden uns jetzt einen Builder schreiben, der dieses DSL erlaubt:

val polygon = builder()
            .withFirstPoint(3.0, 4.0)
            .withColor(Color.AQUA)
            .withSecondPoint(4.0, 7.0)
            .withThirdPoint(1.0, 3.0)
            .withPoint(6.0, 7.5)
            .withPoint(9.0, 2.0)
            .build()

Die Farbe kann an beliebiger Stelle gesetzt werden, und ist erforderlich. Der Aufruf von build() ist auch erst dann möglich, wenn mindestens drei Punkte gesetzt sind. Um die unterschiedliche Benennung der with???Point-Methoden kommen wir leider nicht herum, denn gegen Type-Erasure ist auch Kotlin machtlos.

Für die Farb-Variable benötigen wir ein Interface, wie wir es bereits von den vorigen Beispielen kennen:

interface Value<T>

data class With<T>(val value: T) : Value<T>

class Without<T> : Value<T>

Das Zählen der Punkte übernimmt ein weiteres Interface:

interface Counted

interface None : Counted

interface One : Counted

interface Two : Counted

interface ThreeOrMore : Counted

Nun können wir die Builder-Klasse schreiben:

class Builder<out V : Value<Color>, C : Counted>(val color: V, val points: List<Point2D>) {
    fun withColor(c: Color) = Builder<With<Color>, C>(With(c), points)
}

Moment mal, das ist ein bisschen wenig, oder? Stimmt, die geheime Soße fehlt noch, und zwar in Form von Extension-Methoden:

fun builder() = Builder<Without<Color>, None>(Without(), listOf())

fun <V : Value<Color>> Builder<V, None>.withFirstPoint(x: Double, y: Double) =
        Builder<V, One>(this.color, this.points + Point2D(x, y))

fun <V : Value<Color>> Builder<V, One>.withSecondPoint(x: Double, y: Double) =
        Builder<V, Two>(this.color, this.points + Point2D(x, y))

fun <V : Value<Color>> Builder<V, Two>.withThirdPoint(x: Double, y: Double) =
        Builder<V, ThreeOrMore>(this.color, this.points + Point2D(x, y))

fun <V : Value<Color>> Builder<V, ThreeOrMore>.withPoint(x: Double, y: Double) =
        Builder<V, ThreeOrMore>(this.color, this.points + Point2D(x, y))

fun Builder<With<Color>, ThreeOrMore>.build() = 
        Polygon(this.color.value, this.points)

Man kann sehr schön sehen, wie hier „mitgezählt“ wird. Natürlich wäre es noch schöner, wenn man alle Methoden withPoint nennen könnte, aber wie gesagt macht uns hier Type Erasure einen Strich durch die Rechnung.

Einen Schönheitsfehler hat das Ganze noch: Entgegen der Dokumentation war es mir nicht möglich, den Konstruktor von Builder private zu machen, die Extension-Methoden bekamen keinen Zugriff darauf, obwohl sie in der gleichen Datei standen.

Type-Level-Programming-Techniken wie die hier gezeigten sind nicht nur für Builder, sondern für alle möglichen DSLs interessant. Counted ist ein sogenannter Phantom-Typ, der niemals reale Werte hat, sondern nur dazu dient, zusätzliche „Garantien“ für einen Typ zu codieren. So könnte man SQL-Abfragen, die von einem Client kommen, mit einem Phantom-Typ versehen, der anzeigt, ob man sie schon auf SQL-Injection-Versuche getestet hat oder nicht – und dann kann man diesen Test nicht mehr vergessen, egal wo solche Werte im System herumschwirren.

So, das war jetzt nach – ahäm – längerer Pause endlich wieder mal ein Beitrag, und es hat Spaß gemacht, ihn zu schreiben. Ich denke, ich sollte wieder ein bisschen regelmäßiger bloggen…

Advertisements

Wasch mich, aber mach mich nicht nass!

Es ist kein Geheimnis: Java macht keinen richtigen Spaß mehr, wenn man es mit anderen modernen Sprachen vergleicht. Was macht man, wenn man weiter die JVM und die unzähligen Java-Bibliotheken nutzen will? Richtig, man schaut kräftig beim Marktführer Scala ab, und schreibt eine eigene Sprache. Die natürlich besser ist. So geschehen bei Gosu, RedHat’s Ceylon und nun JetBrain’s Kotlin.

Ich finde das traurig. Richtig, es heißt „Konkurrenz belebt das Geschäft“, aber diese drei Sprachen sind unnötig wie ein Kropf. Es stimmt, Scala kann kompliziert sein und hat auch einige weniger schöne Stellen. Auf der anderen Seite ist es einsteigerfreundlich, innovativ und mächtig, aber vor allem ist es da. Es hat lange gebraucht, um wirklich Bewegung in die Sache zu bringen, eine Community aufzubauen, Tool-Support zu organisieren, ja überhaupt wahrgenommen zu werden. Was inzwischen fast wie ein Selbstläufer aussieht, ist das Ergebnis langer, harter Arbeit. Ich denke, die neuen Sprachschöpfer unterschätzen diesen Aspekt einer Sprache ganz gewaltig.

Viel eigene Innovation ist bei keinem der Kandidaten zu sehen. Aber was mich wirklich stört, ist der Versuch, sich nur die Rosinen aus dem Kuchen zu picken. Das ist nämlich die Art von Geisteshaltung, die Java’s Stillstand erst verursacht hat. Eine gute Sprache ist offen für neue Entwicklungen, und lenkt diese in geordnete Bahnen. Gerade diese Offenheit macht Scala so attraktiv. Doch die Strategie der neuen Sprachen ist die gleiche wie Java: Wasch mich, aber mach mich nicht nass! Features, die „zu kompliziert“ sind oder eventuell missbraucht werden können, werden abgelehnt. In Java waren das etwa Operator-Überladungen, Closures oder Konzepte zur Erweiterung von Interfaces (wie Extensionsmethoden oder Mixins). Jetzt werden aus den gleichen fadenscheinigen Gründen Dinge wie implizite Umwandlungen, abstrakte Typ-Member oder Typpolymorphismus höherer Ordnung abgelehnt – obwohl diese Ideen längst den Praxistest bestanden haben. Was immer wieder übersehen wird ist, dass viele Features, die für die tägliche Arbeit unnötig scheinen, in Bibliotheken und Frameworks essentiell sein können: Jeder, der in Scala eine Liste mapped, verwendet dabei Typpolymorphismus höherer Ordnung – aber er muss dazu nicht einmal wissen, was das ist.

Ich denke dass für viele Detail-Lösungen der neuen Sprachen auch Platz in Scala gewesen wäre, wenn die Macher auf Kooperation gesetzt hätten. Aber jetzt wird viel Arbeit und Gehirnschmalz in Dinge investiert, die es zum größten Teil schon gibt, und Projekte gestartet, die keinen Erfolg haben können. Ein Blick in die Geschichte zeigt, was mit gut gemeinten, aber zu konservativen Ansätzen passiert: Nice hatte viele gute Ideen und nette Details, aber nicht genug, um die Java-Community wirklich zu begeistern, und neue Perspektiven zu eröffnen.

Ich will kein aufgehübschtes Java, und auch kein weichgespültes Scala. Sicher ist es nett, wenn hier und da eine Syntax-Kante geglättet wird. Aber wenn mir am Ende die Ausdrucksstärke fehlt, um meine Gedanken in Code umzusetzen, nützt mir das alles nichts. Ich will keine Sprache zwischen Java und Scala – wenn überhaupt, dann zwischen Scala und Haskell, mit mehr Abstraktionsmöglichkeiten, nicht weniger. Scala ist sicher nicht der Endpunkt der Entwicklung, aber es schwimmt in die richtige Richtung. Nass, aber sauber.

Update

Hier noch ein interessanter Blog-Post, der in die gleiche Richtung und dabei etwas mehr ins Detail geht: Scala, Kotlin, Ceylon… let’s start by being honest.