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.

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