Implizite Parameter mit Default


Heute will ich einen kleinen Trick bei der Verwendung impliziter Parameter vorstellen. Zuerst einmal brauchen wir ein Trait, ein ein paar implizite Objekte, und eine Methode, die diese als implizite Parameter verwendet:

trait PrettyPrinter[-T] {
  def format(t: T):String
}

implicit object IntPrettyPrinter extends PrettyPrinter[Int] {
  def format(t: Int) = if (t < 4) List("zero","one","two","three")(t) else "" + t
}

implicit object CSPrettyPrinter extends PrettyPrinter[CharSequence] {
  def format(t: CharSequence) = "'" + t + "'"
}

def printList[T](list:List[T])(implicit pp:PrettyPrinter[T]) {
    println(list.map(pp.format).mkString("(", " ,", ")"))
}

Unser IntPrettyPrinter gibt ein paar ausgewählte Zahlen als Worte aus, während der CSPrettyPrinter bei der Ausgabe einfache Anführungszeichen hinzufügt:

printList(List(1,2,3,4,5))
//--> (one ,two ,three ,4 ,5)
printList(List("a","b","c","d"))
//--> ('a' ,'b' ,'c' ,'d')

Vielleicht ist schon jemanden das [-T] in PrettyPrinter aufgefallen, das T kontravariant macht. Das hat hier einen positiven Effekt, denn nur so wird für einen String der implizite Parameter einer Oberklasse (oder eines „Ober-Traits“ wie hier CharSequence) akzeptiert. Lässt man das unscheinbare Minus weg, meckert der Compiler:

error: could not find implicit value for parameter pp: prettyprinter.PrettyPrinter[java.lang.String]

Damit spart uns die Kontravarianz eine Menge Arbeit, denn wir brauchen nicht für String, StringBuffer, StringBuilder u.s.w. eigene implizite Objekte zu schreiben, wenn es auch ein gemeinsames implizites Objekt für eine Oberklasse tun würde. Die Sache hat aber einen Haken: Angenommen, wir wollen zusätzlich für alle unbekannten Klassen einen allgemeinen PrettyPrinter verwenden, der einfach nur toString vom jeweiligen Wert zurückgibt:

...

implicit object AnyPrettyPrinter extends PrettyPrinter[Any] {
  def format(t: Any) = t.toString
}

...

Aber jetzt schlägt uns ausgerechnet die Kontravarianz ein Schnippchen:

 both object AnyPrettyPrinter in object prettyprinter of type object scalatest.prettyprinter.AnyPrettyPrinter
 and object IntPrettyPrinter in object prettyprinter of type object scalatest.prettyprinter.IntPrettyPrinter
 match expected type scalatest.prettyprinter.PrettyPrinter[Int]

… und das gleiche nochmal für String. Der Compiler kann sich nicht zwischen der spezielleren Version (für Int bzw. CharSequence) und der allgemeinen Version (Any) entscheiden.

Und jetzt kommt der kleine Trick, von dem ich gesprochen habe: Man lässt einfach das implicit bei AnyPrettyPrinter weg, und gibt ihn statt als impliziten Parameter als Default-Parameter mit:

...

object AnyPrettyPrinter extends PrettyPrinter[Any] {
  def format(t: Any) = t.toString
}

def printList[T](list:List[T])(implicit pp:PrettyPrinter[T] = AnyPrettyPrinter) {
  println(list.map(pp.format).mkString("(", " ,", ")"))
}

Damit funktioniert der Code für alle Klassen: Bei denen mit speziellem PrettyPrinter nimmt er diesen, ansonsten AnyPrettyPrinter.

Advertisements

2 Gedanken zu “Implizite Parameter mit Default

  1. Hier ist ein Detail, das ich nicht nur in vielen Beispielen im Internet gesehen habe, sondern das mir so auch auf der Uni beigebracht wurde: Die Umwandlung einer Integer i in einen String durch "" + i. Wozu eine String-Konkatenation verschwenden? Wozu der zusätzliche implizite StringBuilder? In Scala macht man das per i.toString, und in Java per Integer.toString(i).

  2. Ob es wirklich eine Verschwendung ist, kann nur der Decompiler beantworten, aber ich vermute mal, dass das in so einfachen Fällen wegoptimiert wird.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s