Intuitive APIs mit impliziten Parametern

Will man eine Liste von Paaren aufspalten, gibt es unzip:

println(List((1,"one"),(2,"two")).unzip)
//--> (List(1, 2),List(one, two))

Was ist, wenn man eine Liste von Either hat? Nun, man kann „etwas ähnliches“ schreiben:

def splitEither[A,B](list: List[Either[A,B]]):(List[A],List[B]) =
       (list.collect{case Left(a) => a}, list.collect{case Right(b) => b})

println(splitEither(List[Either[Int,String]](Left(42),Right("two"))))
//--> (List(42),List(two))

Und wenn man einen anderen Datentyp hat, der sich zum Splitten anbietet, etwa Alternativen, dann schreibt man sich eben eine weitere Methode…

Aber zehn Methoden, die alle fast dasselbe tun, sehen nicht besonders chic aus, oder? Und wenn ich beim Refactoring zu einem anderen Datentyp wechsle, darf ich auch überall eine andere split-Methode verwenden. Aber es geht besser, und zwar mit impliziten Parametern. Genauer gesagt implementieren wir das „Type Class Pattern“. Zuerst einmal überlegen wir uns, wie ein Hilfs-Objekt für unsere generische split-Methode aussehen müsste:

trait Splitter[A,B,C] {
   def splitList(list: List[C]):(List[A],List[B])
}

Die split-Methode ist dann trivial:

def split[A,B,C](list:List[C])(implicit splitter:Splitter[A,B,C]):(List[A],List[B]) = splitter.splitList(list)

Hier hatte ich eine kleine Denkblockade, weil sich der implizite Parameter nicht geeignet mit impliziten Objekten oder vals belegen lässt, weil dort natürlich alle Parameter an konkrete Typen gebunden sein müssen, also keine Typ-Parameter zulassen. Oder vornehm ausgedrückt: „Objekte und vals sind in Scala monomorph“.

Zum Glück hat mir Miles Sabin mit seiner Antwort auf Stackoverflow auf die Sprünge geholfen: Es geht ganz einfach mit impliziten Methoden, bei denen die Typ-Parameter kein Problem sind. Irgendwie hatte ich verdrängt, dass implizite Methode nicht nur Typkonvertierungen erlauben, sondern auch implizite Parameter liefern können. Hier der gesamte Code:

 
object splitTest {

  trait Splitter[A,B,C] {
     def splitList(list: List[C]):(List[A],List[B])
  }

  implicit def pairSplitter[A, B] = new Splitter[A, B, Pair[A, B]] {
    override def splitList(list: List[Pair[A, B]]) : (List[A], List[B]) =
    (list.collect{case (a,_) => a}, list.collect{case (_,b) => b})
  }

  implicit def eitherSplitter[A, B] = new Splitter[A, B, Either[A,B]]() {
     override def splitList(list: List[Either[A,B]]):(List[A],List[B]) =
       (list.collect{case Left(a) => a}, list.collect{case Right(b) => b})
  }

  def split[A,B,C](list:List[C])(implicit splitter:Splitter[A,B,C]):(List[A],List[B]) = 
     splitter.splitList(list)

  def main(args: Array[String]) {
    println(split(List((1,"one"),(2,"two"))))
    println(split(List[Either[Int,String]](Left(42),Right("two"))))
    //println(splitList(List(1,2,3,4))) //compiliert nicht
  }

}

Natürlich ist das einiger Aufwand, die ganze Maschinerie in Stellung zu bringen. Gelohnt wird es einen mit Erweiterbarkeit und Flexibilität: Man kann jederzeit Splitter für andere Datentypen schreiben, und diese entweder wie vorgestellt implizit machen, oder split explizit mitgeben. Für den wichtigeren Aspekt halte ich allerdings die Verringerung der Namen, die man sich merken muss. Die Methode split „tut einfach das Richtige“, jedenfalls solange sie weiß, wie sie es anstellen muss. Statt in der API nachzusehen, wie denn die für meinen Fall zutreffende von zehn split-Versionen heißt (so es sie denn überhaupt gibt), kann ich einfach ausprobieren, ob split mit meinem Typ klarkommt – und das schon typsicher beim Compilieren.

Natürlich war das nur ein ganz elementares Beispiel, aber es zeigt trotzdem, welche kreativen Möglichkeiten man bei der Gestaltung einer API hat, und wie man dem Nutzer damit das Leben leichter machen kann.

Werbeanzeigen

Context und View Bounds (von Daniel C. Sobral)

Auf Stackoverflow habe ich eine wirklich druckreife Erklärung von Context und View Bounds von Daniel C. Sobral gefunden, der so freundlich war, mich diese übersetzen und verwenden zu lassen:

Was ist eine View Bound?

View Bounds sind ein Mechanismus, der in Java eingeführt wurde, damit man einen Typ A so verwenden kann, als ob es ein Typ B wäre. Die typische Syntax ist:


def f[A <% B](a: A) = a.bMethod

Mit anderen Worten, A sollte eine implizite Umwandlung nach B besitzen, so dass man Methoden von B auf einer Instanz vom Type A aufrufen kann. Die gebräuchlichste Verwendung von View Bounds in der Standardbibliothek (jedenfalls vor Scala 2.8.0), war mit dem Ordered-Trait, in etwa so:


def f[A <% Ordered[A]](a: A, b: A) = if (a < b) a else b

Da man ein A in ein Ordered[A] umwandeln kann, und weil Ordered[A] die Methode <(other: A): Boolean definiert, kann man den Ausdruck a < b verwenden.

Was ist eine Context Bound?

Context Bounds wurden in Scala 2.8.0 eingeführt, und werden typischerweise mit dem sogenannten Typklassen-Pattern verwendet, einem Code-Pattern, dass die Funktionalität von Haskells Typklassen emuliert, wenn auch in einer etwas umständlicheren Art und Weise.

Während eine View Bound mit einfachen Typen (etwa A <% String) verwendet werden kann, benötigt eine Context Bound einen parameterisierten Typ, wie etwa Ordered[A] weiter oben, aber nicht wie String.

Eine Context Bound beschreibt einen impliziten Wert anstelle der impliziten Umwandlung bei den View Bounds. Sie wird verwendet, um anzuzeigen, dass für einen Typ A ein impliziter Wert des Typs B[A] vorhanden ist. Die Syntax sieht so aus:


def f[A : B](a: A) = g(a) // wobei g einen impliziten Wert des Typs B[A] benötigt

Das ganze ist etwas verwirrender als eine View Bound, weil nicht unmittelbar klar ist, wie man so etwas benutzt. Ein gebräuchliches Anwendungsbeispiel in Scala ist:


def f[A : ClassManifest](n: Int) = new Array[A](n)

Für eine Array-Initialisierung eines parameterisierten Typs muss ein ClassManifest vorhanden sein – aus ziemlich obskuren Gründen im Zusammenhand mit Type Erasure und dem damit nicht kompatiblen Verhalten von Arrays.

Ein anderes verbreitetes Beispiel in der Standardbibliothek ist etwas komplizierter:


def f[A : Ordering](a: A, b: A) = implicitly[Ordering[A]].compare(a, b)

Hier wird die Methode implicitly verwendet, um den impliziten Wert zurückzugewinnen, den wir benötigen, in unserem Fall vom Typ Ordering[A], der die Methode compare(a: A, b: A): Int definiert.

Weiter unten werden wir eine weitere Realisierungsmöglichkeit kennen lernen.

Wie sind View Bounds und Context Bounds implementiert?

Es sollte angesichts ihrer Definition nicht überraschen, dass sowohl View Bounds wie auch Context Bounds mittels impliziter Parameter realisiert werden. In Wahrheit ist die von mir vorgestellte Schreibweise syntaktischer Zucker für die tatsächlichen Vorgänge. So sieht diese „entzuckert“ aus:


def f[A <% B](a: A) = a.bMethod
def f[A](a: A)(implicit ev: A => B) = a.bMethod

def g[A : B](a: A) = h(a)
def g[A](a: A)(implicit ev: B[A]) = h(a)

Dementsprechend kann man stets die volle Syntax verwenden, was insbesondere bei Context Bounds nützlich sein kann:


def f[A](a: A, b: A)(implicit ord: Ordering[A]) = ord.compare(a, b)

Wofür werden View Bounds verwendet?

View Bounds verwendet man hauptsächlich, um die Vorteile des „Pimp My Library“ Patterns zu nutzen, vobei man Methoden zu einer existierenden Klasse „hinzufügen“ will, aber irgendwie den ursprünglichen Typ zurückgeben will. Wenn man den Original-Typ nicht zurückgeben will, benötigt man keine View Bounds.

Das klassische Beispiel für die Verwendung von View Bounds ist der Umgang mit Ordered. Beispielsweise ist Int nicht Ordered, aber es gibt eine implizite Umwandlung. Das weiter oben gegebene Beispiel benötigt eine View Bound genau deshalb, weil dort der nicht konvertierte Typ zurückgegeben wird:


def f[A <% Ordered[A]](a: A, b: A): A = if (a < b) a else b

Dieses Beispiel würde ohne View Bounds nicht funktionieren. Wenn man allerdings einen anderen Typ zurückgibt, benötigt man keine View Bounds mehr:


def f[A](a: Ordered[A], b: A): Boolean = a < b

Die Konvertierung (sofern notwendig) geschieht vor der Übergabe des Parameters an f, so dass f darüber nichts wissen muss.

Neben Ordered ist einer der häufigsten Anwendungsfälle die Verwendung der Java-Klassen String und Array als ob sie Scala Collections wären. Zum Beispiel:


def f[CC <% Traversable[_]](a: CC, b: CC): CC = if (a.size < b.size) a else b

Wenn man versuchen würde, das ohne View Bounds zu schreiben, wäre der Rückgabetyp für einen String WrappedString (in Scala 2.8), und analog bei einem Array.

Das gleiche passiert selbst dann, wenn der Typ nur als Typ-Parameter des Rückgabetyps verwendet wird:


def f[A <% Ordered[A]](xs: A*): Seq[A] = xs.toSeq.sorted

Wofür werden Context Bounds verwendet?

Context Bounds werden hauptsächlich für eine Technik verwendet, die man in Anlehnung an Haskells Typklassen „Typklassen-Pattern“ getauft hat. Grundsätzlich bietet dieses Pattern eine Alternative zur Vererbung, in dem es die Funktionalität eine Art „impliziten Adapter“ bereitstellt.

Das klassische Beispiel ist Ordering in Scala 2.8, das Ordered in der gesamten Scala-Bibliothek ersetzt hat. Die Verwendung ist:


def f[A : Ordering](a: A, b: A) = if (implicitly[Ordering[A]].lt(a, b)) a else b

Allerdings schreibt man es normalerweise in dieser Form:


def f[A](a: A, b: A)(implicit ord: Ordering[A]) = {
    import ord._
    if (a < b) a else b
}

Dabei wird ausgenutzt, dass innerhalb von Ordering einige implizite Konvertierungen bereitgestellt werden, die die traditionellen Verwendung von Operatoren unterstützen. Ein weiteres Beispiel in Scala 2.8 ist Numeric:


def f[A : Numeric](a: A, b: A) = implicitly[Numeric[A]].plus(a, b)

Ein komplizierteres Beispiel ist die Verwundung von CanBuildFrom für die neuen Collections, aber es gibt [auf Stackoverflow] bereits eine ziemlich ausführliche Antwort darauf, so dass ich hier nicht darauf eingehe. Und wie bereits erwähnt gibt es die Verwendung von ClassManifest, die beispielsweise zur Initialisierung von Arrays ohne konkrete Typen benötigt wird.

Es ist viel wahrscheinlicher, dass Context Bounds mit dem Typklassen-Pattern für eigene Klassen verwendet werden, weil man damit die Trennung von Verantwortlichkeiten [Seperation of Concerns] erreichen kann, während man die Verwendung von View Bounds im eigenen Code durch gutes Design vermeiden kann (sie werden meist verwendet, um mit einem fremden Design zurechtzukommen).

Obwohl es sie schon eine ganze Weile gibt, hat sich die Verwendung von Context Bounds erst 2010 richtig durchgesetzt und ist nun zu einem gewissen Grad in vielen von Scalas wichtigsten Bibliotheken und Frameworks zu finden. Die weitestgehendste Verwendung findet sich in der Scalaz-Bibliothek, die eine Menge Haskell-Power in Scala verfügbar macht. Ich kann nur eine Recherche zum Typklassen-Pattern empfehlen, um sich mit dessen verschiedenen Einsatzmöglichkeiten vertraut zu machen.

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.