Bestehende Typen erweitern

Scala Bytes: Pimp My Library

Arno Haase

„Scala Bytes“ ist eine lose Serie von Artikeln über Scala. Diese erste Folge beschreibt, wie man mittels Implicits Datentypen erweitern kann, ohne ihren Code zu verändern – und wann das überhaupt sinnvoll ist.

Bei eigenen Klassen kann man leicht zusätzliche Methoden hinzufügen, bei Bibliotheksklassen nicht. Das führt in Java dazu, dass viele Projekte ihre jeweils eigenen StringHelper-Klassen haben, die statische Hilfsmethoden für den Umgang mit Zeichenketten definieren. In Scala kann man bestehende Klassen nahtlos erweitern, indem man eine zusätzliche Wrapper-Klasse und eine implicit-Methode zur Konvertierung schreibt:

class RichString (self: String) {
  def toFirstLower = self(0).toLowerCase + self.substring(1)
}
object RichString {
  implicit def asRichString (s: String) = new RichString (s)
}

Die Klasse RichString definiert die Methode toFirstLower, die den ersten Buchstaben eines Strings klein macht. (Und ja, sie deckt nicht alle Fälle ab.) Im Companion Object steht eine implicit-Methode zur Konvertierung, die man nur noch einbinden muss:

import RichString._
println ("ABC".toFirstLower)

Damit ist de facto die String-Klasse um eine Methode erweitert. Dieses Idiom ist unter dem Namen „Pimp My Library“ bekannt geworden, weil man damit bestehende Bibliotheken aufmotzen kann [1] .

Einfachere Syntax für eigene Klassen

Analog dazu kann man die Verwendung eigener Klassen syntaktisch vereinfachen. Betrachten wir zum Beispiel eine Klasse Bruch, die eine Methode zum Multiplizieren mit einem anderen Bruch hat:

class Bruch (val z: Int, val n: Int) {
  def * (b: Bruch) = new Bruch (z*b.z, n*b.n)
  ...
}

Instanzen von Bruch sind logisch gesehen Zahlen. Nehmen wir also an, man soll Brüche z. B. mit Int oder Double multiplizieren können:

val einHalb = new Bruch (1, 2)
val a: Bruch = 3 * einHalb // TODO
val x: Double = 0.8 * einHalb // TODO

Beide Fälle lassen sich nicht durch zusätzliche Methoden in Bruch erledigen, weil der Int bzw. Double links vom Operator steht. Implicit-Methoden im Companion Object sorgen aber für die nötigen Typumwandlungen:

object Bruch {
  implicit def intAlsBruch (i: Int) = new Bruch (i, 1)
  implicit def bruchAlsDouble (b: Bruch) = (b.z * 1.0) / b.n
}

Wenn man sie importiert, kann man Int-Werte als Brüche und Brüche als Doubles behandeln.

Risiken und Nebenwirkungen

Implicits sind ein extrem ausdrucksstarkes Sprachmittel. Deshalb kann man mit ihnen auch Quelltext schreiben, der völlig anders aussieht, als man es im ersten Augenblick erwartet, und den man als Nichteingeweihter nur schwer verstehen kann. Deshalb ist die wichtigste Regel für den Umgang mit Implicits, Überraschungen beim Leser zu vermeiden. Man sollte nur das syntaktisch zusammenrücken, was auch semantisch zusammengehört. Außerdem sollte man sie nur dort einsetzen, wo zusätzliche normale Methoden nicht möglich sind – die Integration von Fremdbibliotheken ist oft ein gutes Beispiel. Gute Beispiele stehen in der Scala-Standardbibliothek, die mit Implicits Arrays und Strings in das Collection-Framework integriert [2] sowie die primitiven Typen um Methoden erweitert – die entsprechenden Implicits stehen im Objekt scala.Predef. Zur Vermeidung von Wildwuchs beschränkt Scala streng die Situationen, in denen Implicits wirksam werden. Dabei gelten folgende Regeln:

  1. Implizite Konvertierungen werden nur angewendet, wenn der Code ohne sie ungültig wäre.
  2. Implicits werden nur angewendet, wenn sie „im Scope“ sind, wenn man sie also ohne vorangestellten Punkt verwenden könnte.
  3. Wenn es mehrere Implicit-Definitionen gibt, die zu gültigem Code führen würden, bricht der Compiler mit einem Fehler ab. (Die genauen Regeln sind etwas differenzierter
    [2]

    [3]

  4. Implicits werden nie mehrstufig angewendet. Wenn es also einen Implicit von A nach B und einen zweiten von B nach C gibt, wird A trotzdem nicht in C konvertiert.

Die letzten beiden Regeln helfen dabei, den Überblick zu behalten und Überraschungen auf ein Minimum zu beschränken.

Fazit

Mit Implicits kann man die Lesbarkeit von Quelltexten erheblich verbessern, indem man scheinbar bestehende Typen um zusätzliche Methoden erweitert. Das bringt aber natürlich die Verantwortung mit sich, Einfachheit und Verständlichkeit anzustreben und verspielte Selbstverliebtheit zu vermeiden. Wenn man sie sinnvoll einsetzt, ist diese Technik aber ein ungemein nützliches Werkzeug, um Code intuitiv erfassbar zu machen.

Arno Haase ist selbstständiger Softwarearchitekt aus Braunschweig. Neben seiner Beratungstätigkeit krempelt er gerne die Ärmel hoch und schreibt selbst Software, sowohl in Kundenprojekten als auch Open Source. Er ist eines der Gründungsmitglieder von se-radio.net, dem Software Engineering Podcast. Außerdem spricht er regelmäßig auf einschlägigen Konferenzen und ist Autor diverser Artikel und Patterns sowie Koautor von Büchern über JPA und modellgetriebene Softwareentwicklung.
Geschrieben von
Arno Haase
Kommentare

Schreibe einen Kommentar

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