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

Zwilligsparadoxon dank Type Erasure

Eine häufige Frage ist, was man macht, wenn eine Methodenüberladung „dank“ Type Erasure nicht funktioniert, etwa soetwas:

  def foo(p:List[String]) { println("Strings") }
  def foo(p:List[Int]) { println("Ints") }

Der Compiler sieht hier nur zwei Methoden mit dem gleichen Argument des Typs „List“ und meckert:

Double definition: method foo:(List[String])Unit and method foo:(List[Int])Unit have same type after erasure: (List)Unit.

Für dieses Problem gibt es mehrere Lösungen, z.B, mit impliziten Parametern:

//Idee von Michael Krämer
implicit val s = ""
mplicit val i = 1

def foo(p:List[String])(implicit ignore: String) { println("Strings") }
def foo(p:List[Int])(implicit ignore: Int) { println("Ints") }

Eine andere Variante ist, die Listen-Typen selbst implizit umwandeln zu lassen:

case class IntList(list: List[Int])
case class StringList(list: List[String])

implicit def il(list: List[Int]) = IntList(list)
implicit def sl(list: List[String]) = StringList(list)

def foo(i: IntList) { println("Ints")}
def foo(s: StringList) { println("Strings") }

Der „offensichtliche“ Weg, für jede Version ein Manifest mitzugeben, funktioniert leider nicht, denn Manifest (oder ClassManifest) unterliegt selbst dem Type Erasure – wir haben also nichts gekonnt. Trotzdem gibt es hier einen Weg, Manifests zu verwenden: Wir lassen den Compiler Manifests zählen.

def foo(p: List[String]) { println("Strings") }
def foo[X: ClassManifest](p: List[Int]) { println("Ints") }
def foo[X: ClassManifest, Y: ClassManifest](p: List[Double]) { println("Doubles") }

Natürlich wird das schnell unübersichtlich, man spart sich aber das Hantieren mit impliziten Parametern. Die Typ-Parameter und Manifests spielen im Prinzip keine Rolle, es kommt nur darauf an, dass die Argumentlisten von foo nun unterschiedliche Längen haben, denn intern wird ja jedes ClassManifest zu einen zusätzlichen impliziten Parameter.

Wenn jemand eine hübschere Lösung weiß, möge er sie bitte mitteilen.

Ich habe übrigens die Scala-Link-Seite aktualisiert: Neuer Artikel bei „What’s new in Scala 2.8“, Martin Odersky hat die Firma „Scala Solutions“ für professionellen Scala-Support gegründet, zwei Cheat-Sheets und FeedCluster für Scala hinzugefügt.

Nachtrag

Wenn man genau zwei Methoden hat, gibt es folgende elegante Lösung:

def foo(list: => List[Int]) = { println("Int-List " + list)}
def foo(list: List[String]) = { println("String-List " + list)}