2013-08-16 13 views
6

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?

+2

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. –

+3

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. –

Antwort

5

Die Art und Weise würden Sie normalerweise einen algebraischen Datentyp in Scala implementieren würde mit case Klassen:

sealed trait Employee 
case class Packer(boxesPacked: Int, cratesPacked: Int) extends Employee 
case class Shipper(trucksShipped: Int) extends Employee 

Dies gibt Muster Extraktoren für den Packer und Shipper Bauer, so dass Sie auf sie mithalten können.

Leider sind Packer und Shipper auch verschiedene (Unter) -Typen, aber ein Teil des Musters der Codierung eines algebraischen Datentyps in Scala ist diszipliniert darüber, dies zu ignorieren. Stattdessen wird, wenn zwischen einem Packer oder Absender zu unterscheiden, verwenden Sie Pattern-Matching, wie Sie es in Haskell:

implicit object EmployeeReportMaker extends ReportMaker[Employee] { 
    def printReport(e: Employee) = e match { 
    case Packer(boxes, crates) => // ... 
    case Shipper(trucks)  => // ... 
    } 
} 

Wenn Sie keine anderen Arten haben, für die Sie brauchen eine ReportMaker Instanz, dann vielleicht die Typklasse ist nicht erforderlich, und Sie können einfach die printReport Funktion verwenden.

+0

Dies löst das Problem nicht, da Typklassen nur verwendet werden, wenn Sie wissen, dass später weitere Datentypen hinzugefügt werden müssen. Zum Beispiel könnte der Autor des Mitarbeitercodes denken, dass irgendwo später jemand eine Manager-Klasse hinzufügen muss. Da Sie jedoch das Mustervergleichsverfahren zur Lösung des Problems verwenden, kann er die Klasse nur hinzufügen, wenn er den Bibliothekscode selbst ändert. Was er vielleicht nicht kann. – DrPepper

0

jedoch ein Verfahren zu schreiben, die, ohne explizit die rm-Objekt zu speichern wäre unmöglich, aus dem Bericht eines jeden Mitarbeiters in den Kader zu drucken versuchen würde, die reportAndAdd geben wird!

Nicht sicher von Ihrem genauen Problem. Die folgenden sollte funktionieren (natürlich mit getrennten Berichten an der I/O-Ausgangspunkt verketteten):

def printReport(ls: List[Employee]) = { 
    def printReport[T <: Employee](e: T)(implicit rm: ReportMaker[T]) = rm.printReport(e) 
    ls foreach(printReport(_)) 
} 

Aber ich tun/O irgendwo auf der Methode-call-Baum (oder in Methoden iterativ genannt) ist gegen die "funktionale Philosophie". Es ist besser, einzelne Unterberichte als String/List [String]/andere präzise Strukturen zu erstellen, sie alle bis zur äußersten Methode zu blasen und I/O in einem einzigen Treffer auszuführen. Z.B .:

trait ReportMaker[T] { 
    def generateReport(t: T): String 
} 

(Insert implizite Objekte ähnlich wie Q ...)

def printReport(ls: List[Employee]) = { 
    def generateReport[T <: Employee](e: T)(implicit rm: ReportMaker[T]): String = rm.generateReport(e) 
    // trivial example with string concatenation - but could do any fancy combine :) 
    someIOManager.print(ls.map(generateReport(_)).mkString("""\n"""))) 
}