Ich betrachte Typklassen als die Möglichkeit, einer Klasse typsichere Metadaten hinzuzufügen.
Sie definieren also zunächst eine Klasse, um den Problembereich zu modellieren, und denken dann über Metadaten nach, die Sie ihr hinzufügen können. Dinge wie Equals, Hashable, Viewable, etc. Auf diese Weise wird eine Trennung zwischen dem Problembereich und den Mechanismen zur Verwendung der Klasse geschaffen und die Möglichkeit der Unterklassifizierung eröffnet, da die Klasse schlanker ist.
Abgesehen davon können Sie Typklassen überall im Anwendungsbereich hinzufügen, nicht nur dort, wo die Klasse definiert ist, und Sie können Implementierungen ändern. Wenn ich beispielsweise mit Point#hashCode einen Hash-Code für eine Point-Klasse berechne, bin ich auf diese spezifische Implementierung beschränkt, die möglicherweise keine gute Verteilung der Werte für die spezifische Menge von Points, die ich habe, erzeugt. Wenn ich jedoch Hashable[Point] verwende, kann ich meine eigene Implementierung bereitstellen.
[Aktualisiert mit Beispiel] Hier ist ein Beispiel für einen Anwendungsfall, den ich letzte Woche hatte. In unserem Produkt gibt es mehrere Fälle von Maps, die Container als Werte enthalten. Z.B., Map[Int, List[String]]
o Map[String, Set[Int]]
. Das Hinzufügen zu diesen Sammlungen kann langwierig sein:
map += key -> (value :: map.getOrElse(key, List()))
Ich wollte also eine Funktion haben, die dies umschließt, damit ich schreiben kann
map +++= key -> value
Das Hauptproblem ist, dass die Sammlungen nicht alle die gleichen Methoden zum Hinzufügen von Elementen haben. Einige haben '+', andere ':+'. Ich wollte auch die Effizienz des Hinzufügens von Elementen zu einer Liste beibehalten, also wollte ich nicht fold/map verwenden, die neue Sammlungen erstellen.
Die Lösung ist die Verwendung von Typklassen:
trait Addable[C, CC] {
def add(c: C, cc: CC) : CC
def empty: CC
}
object Addable {
implicit def listAddable[A] = new Addable[A, List[A]] {
def empty = Nil
def add(c: A, cc: List[A]) = c :: cc
}
implicit def addableAddable[A, Add](implicit cbf: CanBuildFrom[Add, A, Add]) = new Addable[A, Add] {
def empty = cbf().result
def add(c: A, cc: Add) = (cbf(cc) += c).result
}
}
Hier habe ich eine Typklasse definiert Addable
die ein Element C zu einer Sammlung CC hinzufügen kann. Ich habe 2 Standardimplementierungen: Für Listen mit ::
und für andere Sammlungen mit Hilfe des Builder-Frameworks.
Dann ist die Verwendung dieser Typklasse:
class RichCollectionMap[A, C, B[_], M[X, Y] <: collection.Map[X, Y]](map: M[A, B[C]])(implicit adder: Addable[C, B[C]]) {
def updateSeq[That](a: A, c: C)(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = {
val pair = (a -> adder.add(c, map.getOrElse(a, adder.empty) ))
(map + pair).asInstanceOf[That]
}
def +++[That](t: (A, C))(implicit cbf: CanBuildFrom[M[A, B[C]], (A, B[C]), That]): That = updateSeq(t._1, t._2)(cbf)
}
implicit def toRichCollectionMap[A, C, B[_], M[X, Y] <: col
Der besondere Clou ist die Verwendung von adder.add
um die Elemente hinzuzufügen und adder.empty
um neue Sammlungen für neue Schlüssel zu erstellen.
Zum Vergleich: Ohne Typklassen hätte ich 3 Möglichkeiten gehabt: 1. eine Methode pro Sammlungstyp zu schreiben. Z.B., addElementToSubList
y addElementToSet
usw. Dies führt zu einer Menge an Boilerplate in der Implementierung und verschmutzt den Namespace 2. Reflection zu verwenden, um festzustellen, ob die Untersammlung eine Liste / Menge ist. Das ist knifflig, da die Map zu Beginn leer ist (natürlich hilft Scala hier auch mit Manifesten) 3. eine "poor-man's type class" zu haben, indem man den Benutzer auffordert, den Addierer zu liefern. Also etwas wie addToMap(map, key, value, adder)
was einfach hässlich ist