Bei der Arbeit an einem Scala-Projekt, das das Type-Class-Muster verwendete, stieß ich auf ein ernsthaftes Problem bei der Umsetzung des Musters durch die Sprache: Da Scala-Klassenimplementierungen von der Programmierer und nicht die Sprache, kann jede Variable, die zu einer Typklasse gehört, niemals als übergeordneter Typ kommentiert werden, es sei denn, ihre Typklassenimplementierung wird mitgenommen.Probleme bei der Verallgemeinerung von Scala-Klassen
Um diesen Punkt zu veranschaulichen, habe ich ein schnelles Beispielprogramm programmiert. Stellen Sie sich vor, Sie haben versucht, ein Programm zu schreiben, das verschiedene Arten von Mitarbeitern für ein Unternehmen behandeln könnte und Berichte über deren Fortschritt drucken könnte. Um dies zu lösen mit dem Typ-Klasse-Muster in Scala, könnten Sie so etwas wie dies versuchen:
abstract class Employee
class Packer(boxesPacked: Int, cratesPacked: Int) extends Employee
class Shipper(trucksShipped: Int) extends Employee
Eine Klassenhierarchie Modellierung verschiedene Arten von Mitarbeitern, einfach genug. Jetzt implementieren wir die ReportMaker-Typklasse.
trait ReportMaker[T] {
def printReport(t: T): Unit
}
implicit object PackerReportMaker extends ReportMaker[Packer] {
def printReport(p: Packer) { println(p.boxesPacked + p.cratesPacked) }
}
implicit object ShipperReportMaker extends ReportMaker[Shipper] {
def printReport(s: Shipper) { println(s.trucksShipped) }
}
, dass alles gut ist und gut, und wir können jetzt eine Art von Roster Klasse schreiben, die wie folgt aussehen könnte:
class Roster {
private var employees: List[Employee] = List()
def reportAndAdd[T <: Employee](e: T)(implicit rm: ReportMaker[T]) {
rm.printReport(e)
employees = employees :+ e
}
}
So das funktioniert. Jetzt können wir dank unserer Typklasse entweder einen Packer oder ein Verladerobjekt in die reportAndAdd-Methode übergeben, und es wird den Bericht drucken und den Mitarbeiter zum Dienstplan hinzufügen. Es wäre jedoch unmöglich, eine Methode zu schreiben, die versucht, den Bericht jedes Mitarbeiters in der Liste auszudrucken, ohne das rm-Objekt explizit zu speichern, das an reportAndAdd!
Zwei andere Sprachen, die das Muster unterstützen, Haskell und Clojure, teilen dieses Problem nicht, da sie sich mit diesem Problem befassen. Haskell's speichert das Mapping vom Datentyp zur Implementierung global, also ist es immer 'mit' der Variablen und Clojure macht im Grunde dasselbe. Hier ist ein kurzes Beispiel, das in Clojure perfekt funktioniert.
(defprotocol Reporter
(report [this] "Produce a string report of the object."))
(defrecord Packer [boxes-packed crates-packed]
Reporter
(report [this] (str (+ (:boxes-packed this) (:crates-packed this)))))
(defrecord Shipper [trucks-shipped]
Reporter
(report [this] (str (:trucks-shipped this))))
(defn report-roster [roster]
(dorun (map #(println (report %)) roster)))
(def steve (Packer. 10 5))
(def billy (Shipper. 5))
(def roster [steve billy])
(report-roster roster)
Neben der eher unangenehmen Lösung der Mitarbeiterliste in Typenliste Drehen [(Employee, Report [Angestellte]), anbietet Scala jede mögliche Weise, dieses Problem zu lösen? Und wenn nicht, da die Scala-Bibliotheken ausgiebig Typ-Klassen verwenden, warum wurde sie nicht angesprochen?
Dies ist ein weiteres Beispiel für Subtyping alles vermasselt. Wenn Sie Ihre Mitarbeiterunterklassen als Konstruktoren (im Haskell-Sinne) und nicht als Subtypen betrachten, werden Sie feststellen, dass der Typklassenansatz wesentlich komfortabler ist. –
Ich bin mir nicht sicher, ob Haskell dieses Problem so lösen würde, wie Sie denken. Haskells Standardlistentyp ist nicht heterogen, so dass alle Elemente die gleiche Klasseninstanz haben. Die Idee, verschiedene Typen 'Packer' und' Shipper' in derselben Liste zu platzieren, würde einfach nicht funktionieren. –