Podczas pracy nad projektem Scala, który używał wzorca typu klasy, natknąłem się na coś, co wydaje się być poważnym problemem w sposobie implementacji wzorca przez język: Ponieważ implementacje klas typu Scala muszą być zarządzane przez programista, a nie język, żadna zmienna należąca do klasy typu nigdy nie może zostać przypisana jako typ nadrzędny, chyba że zostanie zaimplementowana jego implementacja klasy typu.Problemy generalizujące klasy typu Scala
Aby zilustrować ten punkt, zakodowałem program szybkiego przykładu. Wyobraź sobie, że próbujesz napisać program, który może obsługiwać różnych pracowników dla firmy i może drukować raporty o ich postępach. Aby rozwiązać ten problem z wzorem typu klasy w Scala, można spróbować czegoś takiego:
abstract class Employee
class Packer(boxesPacked: Int, cratesPacked: Int) extends Employee
class Shipper(trucksShipped: Int) extends Employee
modelowania hierarchii klasa różne rodzaje pracowników, dość proste. Teraz implementujemy klasę typu ReportMaker.
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) }
}
To wszystko dobrze, a teraz możemy napisać jakąś klasę Roster, że może wyglądać tak:
class Roster {
private var employees: List[Employee] = List()
def reportAndAdd[T <: Employee](e: T)(implicit rm: ReportMaker[T]) {
rm.printReport(e)
employees = employees :+ e
}
}
Tak to działa. Teraz, dzięki naszej klasie typów, możemy przekazać obiekt pakujący lub nadawca do metody reportAndAdd, a następnie wydrukować raport i dodać pracownika do listy. Jednak pisanie metody, która będzie próbowała wydrukować raport każdego pracownika w liście, byłoby niemożliwe, bez jawnego przechowywania obiektu rm, który zostanie przekazany do reportAndAdd!
Dwa inne języki obsługujące wzorzec, Haskell i Clojure, nie dzielą tego problemu, ponieważ radzą sobie z tym problemem. Haskell przechowuje mapowanie od typu danych do implementacji globalnie, więc zawsze jest "z" zmienną, a Clojure w zasadzie robi to samo. Oto krótki przykład, który działa doskonale w Clojure.
(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)
Oprócz raczej bolesnego rozwiązania obracając listę pracowników do typu listy [(Employee, ReportMaker [Employee]), czy Scala oferuje żadnego sposobu, aby rozwiązać ten problem? A jeśli nie, skoro biblioteki Scala szeroko korzystają z klas typu, dlaczego nie zostało to uwzględnione?
To kolejny przykład podtypów brudząc wszystko. Jeśli myślisz o swoich podklasach pracowniczych jako konstruktorach (w sensie Haskella) zamiast o podtypy, okaże się, że podejście klasy typu jest znacznie wygodniejsze. –
Nie jestem pewien, czy Haskell rozwiązałby ten problem w sposób, w jaki myślisz, że tak. Standardowy typ listy Haskell nie jest heterogeniczny, więc wszystkie elementy będą miały tę samą instancję typu typeclass. Pomysł umieszczenia odrębnych typów 'Packer' i' Shipper' na tej samej liście po prostu by nie zadziałał. –