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.

Advertisements

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