2013-08-16 13 views
6

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?

+2

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

+3

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

Odpowiedz

5

Droga, którą zazwyczaj wdrożyć algebraicznych typ danych w Scala będzie z case klas:

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

Daje extractory wzorzec dla Packer i Shipper konstruktorów, dzięki czemu można dopasować na nich.

Niestety, Packer i Shipper są również odrębnymi (pod) typami, ale część schematu kodowania algebraicznego typu danych w Scali należy zdyscyplinować na temat ignorowania tego. Zamiast tego, gdy rozróżnienia między pakującego lub nadawcy, należy użyć wzoru dopasowanie, jak w Haskell:

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

Jeśli nie ma innych typów, dla których trzeba instancję ReportMaker, to może klasa typ nie jest potrzebny, i możesz po prostu użyć funkcji printReport.

+0

Nie rozwiąże to problemu, ponieważ, jak powiedziałeś, klasy typu są używane tylko wtedy, gdy wiesz, że ktoś będzie potrzebował później dodać więcej typów danych. Na przykład autor kodu pracownika może pomyśleć, że gdzieś w dalszej kolejności ktoś będzie musiał dodać klasę Menedżera. Ale ponieważ używasz dopasowywania wzorców do rozwiązania problemu, nie może on dodać klasy, chyba że sam modyfikuje kod biblioteki. Które może nie być w stanie zrobić. – DrPepper

0

Jednak pisząc metodę, która będzie próbował wydrukować raportu każdego pracownika w zaplanowany byłoby niemożliwe bez wyraźnego przechowywania obiektu rm, która jest przekazywana reportAndAdd!

Nie jestem pewien twojego konkretnego problemu. Poniższy powinien działać (oczywiście z oddzielnych raportach łączone w/wy punktu wyjścia I):

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

jednak robi wejść/wyjść gdzieś w dół metody-call-drzewa (lub w metodach zwanych iteracyjnie) jest przeciw "filozofia funkcjonalna". Lepiej generować poszczególne raporty podrzędne jako ciąg/listę [łańcuch]/inną precyzyjną strukturę, przepuść je wszystkie aż do najbardziej zewnętrznej metody i wykonaj operacje we/wy jednym trafieniem. Np .:

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

(wkładka ukryte obiekty podobne do 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"""))) 
}