Implicit Parameters und Type Classes

Advanced Scala

Heiko Seeberger

Im letzten Teil dieser Serie haben wir Implicit Conversions kennen gelernt. Heute werden wir unseren Blick zunächst auf ein verwandtes Thema richten, auf Implicit Parameters. Anschließend werden wir Type Classes vorstellen, ein derzeit sehr angesagtes Anwendungsbeispiel für die Implicits.

Currying

Um Implicit Parameters zu verstehen, müssen wir zunächst ein anderes fortschrittliches Sprachkonstrukt unter die Lupe nehmen: Currying. Damit bezeichnet man die Möglichkeit, dass Methoden und Funktionen mehrere Parameterlisten haben können, wie im folgenden Beispiel der Addition zweier Int-Werte demonstriert:

scala> val add = (x: Int) => (y: Int) => x + y
add: (Int) => (Int) => Int = <function1>

scala> add(1)(2)
res0: Int = 3

Die Funktion add hat je eine Parameterliste für jeden Summanden und muss auch dementsprechend aufgerufen werden. Natürlich funktioniert das auch mit drei oder mehr Parameterlisten und ebenso mit mehreren Parametern pro Liste.

Mit Blick auf die Definition bzw. den Typ können wir die Funktion add auch anders auffassen, und zwar als eine Funktion mit einem Parameter, die als Ergebnis eine weitere Funktion mit einem Parameter hat. Diese Sichtweise führt nun direkt zum größten Nutzen der “gewürzten” Schreibweise. Wir können beim Aufruf die rechte Parameterliste weglassen und erhalten dadurch eine neue Funktion:

scala> val addOne = add(1)
addOne: (Int) => Int = <function1>

scala> addOne(2)
res1: Int = 3
Implicit Values

Es gäbe noch vieles über das in funktionalen Sprachen beheimatete Currying zu sagen, aber wir wollen uns nun einem speziellen Scala-Feature zuwenden. Wenn wir eine Methode mit mehreren Parameterlisten schreiben, können wir die letzte als implicit kennzeichnen.

scala> def add(x: Int)(implicit y: Int) = x + y
add: (x: Int)(implicit y: Int)Int

Diese Methode können wir zunächst “ganz normal” aufrufen, indem wir beide Parameterlisten verwenden. Aber wir können auch die letzte einfach weglassen und der Compiler wird sie für uns implizit füllen:

	scala> add(2)
	<console>:7: error: could not find implicit value for parameter y: Int
	add(2)
	^

Nun, so einfach geht es doch nicht, denn der Compiler verwendet natürlich keine zufälligen Werte. Hier kommen jetzt die Implicit Values ins Spiel: Wir definieren einfach einen passenden Wert, also einen Int-Wert, mit dem Schlüsselwort implicit, und schon kompiliert unser kleines Beispiel:

scala> implicit val defaultInt = 1
defaultInt: Int = 1

scala> add(2)
res1: Int = 3

Ähnlich wie für Implicit Conversions gelten auch für Implicit Values strenge Regeln. Insbesondere gilt, dass ein Implicit Value im Scope sein muss, sonst wird sich der Compiler beschweren.

Type Classes

Das Konzept der Type Classes ermöglicht eine Form von Polymorphismus orthogonal zur Vererbung. Im Wesentlichen geht es darum, dass wir mit Typen, die eigentlich nichts miteinander zu tun haben, gewisse Dinge gleichermaßen tun können.

Wir wollen gleich ein Beispiel betrachten: Serialisierung nach bzw. Deserialisierung von XML. Ein klassischer OO-Ansatz wäre z. B., eine Methode toXML zu definieren und durch Vererbung bzw. Mix-in in unsere Klassenstruktur hineinzubringen. Aber wir wollen unsere “Domänenobjekte” sicher nicht mit einem solchen Aspekt “verschmutzen”. Daher würden wir vielleicht auf das Adapter-Pattern ausweichen. Aber wenn wir keine “mächtige Hilfe” bekommen, dann bezahlen wir mit viel Glue Code und expliziten Transformationen, wie z. B. im Adaptermodell der Eclipse-Plattform.

Scala bietet uns zum Glück die erforderliche mächtige Hilfe in Form von Implicits.

object XMLSerialization {
def toXML[T](t: T)(implicit format: XMLFormat[T]): NodeSeq =
format toXML t
def fromXML[T](xml: NodeSeq)(implicit format: XMLFormat[T]): T =
format fromXML xml
}

trait XMLFormat[T] {
def toXML(t: T): NodeSeq
def fromXML(xml: NodeSeq): T
}

Das Singleton Object XMLSerialization bietet uns parametrisierte Methoden zum (De-)Serialisieren, d. h. wir können diese für beliebige Typen, übergeben in der ersten Parameterliste, verwenden. Dabei wird die eigentliche Arbeit an ein XMLFormat delegiert, das mit demselben Typ parametrisiert ist und in der zweiten und impliziten Parameterliste übergeben wird.

Betrachten wir nun als konkreten Fall die Klasse Person:

case class Person(firstName: String, lastName: String)

Um diese (de-)serialisieren zu können, müssen wir ein XMLFormat[Person] schreiben:

object Person {
implicit object PersonXMLFormat extends XMLFormat[Person] {
override def toXML(person: Person) = {
import person._
<person firstName = { firstName } lastName = { lastName } />
}
override def fromXML(xml: NodeSeq) =
Person(xml \ "@firstName" text, xml \ "@lastName" text)
}
}

Die Aufgabe ist mit Scalas tollem XML-Support schnell gelöst. Das PersonXMLFormat ist ein Singleton Object, das wir durch Voranstellen von implicit zum Implicit Value machen. Nun können wir folgenden Test schreiben:

class PersonSpec extends Specification {
"Serializing and deserializing a Person" should {
"return an equal Person" in {
val person = Person("Martin", "Odersky")
import XMLSerialization._
import Person._
fromXML(toXML(person)) mustEqual person
}
}
}

Wer mit dem hier verwendeten Specs-Framework nicht vertraut ist, der betrachte einfach nur den inneren Block. Wir importieren XMLSerialization._ , um auf die Methoden toXML und fromXMLzugreifen zu können. Und wir importieren Person._ , um das PersonXMLFormat in den Scope zu bringen. Wenn wir diesen letzten Import auskommentieren, werden wir eine Fehlermeldung vom Compiler erhalten.

Natürlich könnten wir nun weitere Domänenobjekte und passende XMLFormats definieren. Wir könnten auch versuchen, ein generisches XMLFormat zu definieren, das mittels Reflection beliebige Domänenobjekte (de-)serialisieren kann. Dann könnten wir explizit anhand des Imports die Wahl treffen, welches XMLFormat wir heranziehen wollen.

Zusammenfassend kann gesagt werden, dass wir mit Scala Type Classes dank Implicits eine sehr elegante und flexible Möglichkeit haben, Polymorphismus auf Basis von Typparametrisierung umzusetzen.

Der Code für das Type-Classes-Beispiel kann auf github unter http://wiki.github.com/weiglewilczek/demo-scala gefunden werden. Fragen oder Kommentare sind natürlich jederzeit willkommen!

Geschrieben von
Heiko Seeberger
Heiko Seeberger
Heiko Seeberger is Fellow at codecentric. He has been a Scala enthusiast ever since he came to know this fascinating language in 2008. Deeply interested in distributed computing he became a contributor to the amazing Akka project in 2010. Heiko has more than 20 years of professional expertise in consulting and software development and regularly shares his expertise in articles and talks. He is also the author of the German Scala book "Durchstarten mit Scala". Heiko tweets as @hseeberger and blogs under heikoseeberger.de.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.