2013-08-16 9 views
6

Mentre lavoravo su un progetto Scala che usava il pattern Type Class, mi sono imbattuto in quello che sembra essere un problema serio nel modo in cui il linguaggio implementa il pattern: Poiché le implementazioni di classe tipo Scala devono essere gestite dal programmatore e non il linguaggio, qualsiasi variabile appartenente a una classe tipo non può mai diventare annotata come un tipo genitore a meno che la sua implementazione di classe tipo non venga presa con esso.Problemi di generalizzazione delle classi di tipo Scala

Per illustrare questo punto, ho codificato un programma di esempio rapido. Immagina di provare a scrivere un programma in grado di gestire diversi tipi di dipendenti per un'azienda e di stampare rapporti sui loro progressi. Per risolvere questo problema con il modello tipo di classe in Scala, si potrebbe provare qualcosa di simile:

abstract class Employee 
class Packer(boxesPacked: Int, cratesPacked: Int) extends Employee 
class Shipper(trucksShipped: Int) extends Employee 

Una classe gerarchia di modellazione diversi tipi di dipendenti, abbastanza semplice. Ora implementiamo la classe di tipo 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) } 
} 

che è cosa buona e giusta, e ora possiamo scrivere una sorta di classe Roster che potrebbe essere simile a questo:

class Roster { 
    private var employees: List[Employee] = List() 

    def reportAndAdd[T <: Employee](e: T)(implicit rm: ReportMaker[T]) { 
     rm.printReport(e) 
     employees = employees :+ e 
    } 
} 

Quindi questo funziona. Ora, grazie alla nostra classe di tipo, possiamo passare un packer o un oggetto shipper nel metodo reportAndAdd, e stamperà il report e aggiungerà il dipendente al roster. Tuttavia, scrivere un metodo che tenterebbe di stampare il report di ogni dipendente nel roster sarebbe impossibile, senza memorizzare esplicitamente l'oggetto rm che viene passato a reportAndAdd!

Due altri linguaggi che supportano il modello, Haskell e Clojure, non condividono questo problema, poiché si occupano di questo problema. Haskell memorizza la mappatura dal tipo di dati all'implementazione a livello globale, quindi è sempre "con" la variabile, e Clojure fondamentalmente fa la stessa cosa. Ecco un rapido esempio che funziona perfettamente in 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) 

A parte il piuttosto sgradevole soluzione di trasformare la lista dei dipendenti in tipo List [(Employee, ReportMaker [Employee]), non Scala offre alcun modo per risolvere questo problema? E se no, dal momento che le librerie Scala fanno ampio uso di Classi di Tipo, perché non è stato indirizzato?

+2

questo è l'ennesimo esempio di sottotipo incasinare tutto. Se pensate alle sottoclassi dei vostri dipendenti come costruttori (nel senso di Haskell) anziché come sottotipi, scoprirete che l'approccio di tipo classe è molto più comodo. –

+3

Non sono sicuro che Haskell risolverà questo problema nel modo in cui credi che lo farebbe. Il tipo di lista standard di Haskell non è eterogeneo, quindi tutti gli elementi avrebbero la stessa istanza di classe di tipizzazione. L'idea di mettere distinti tipi 'Packer' e' Shipper' nella stessa lista non funzionerebbe. –

risposta

5

Il modo in cui tipicamente si dovrebbe implementare un tipo di dati algebrico a Scala sarebbe stato con case classi:

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

Questo dà estrattori modello per il Packer e Shipper costruttori, in modo da poter abbinare su di loro.

Sfortunatamente, Packer e Shipper sono anche distinti (sub) tipi, ma parte del modello di codifica di un tipo di dati algebrico in Scala deve essere disciplinato sull'ignorare questo. Invece, quando la distinzione tra un software o di spedizioniere, usare il pattern matching, come si farebbe in Haskell:

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

Se si dispone di nessun altro tipo per i quali è necessario un esempio ReportMaker, allora forse la classe tipo non è necessaria, e puoi semplicemente usare la funzione printReport.

+0

Questo non risolve il problema perché, come hai detto, le classi di tipi vengono utilizzate solo quando sai che qualcuno dovrà aggiungere più tipi di dati in seguito. Ad esempio, l'autore del codice dipendente potrebbe pensare che da qualche parte in poi qualcuno dovrà aggiungere una classe Manager. Ma dal momento che stai usando la corrispondenza dei modelli per risolvere il problema, non può aggiungere la classe a meno che non modifichi il codice della libreria stesso. Quale potrebbe non essere in grado di fare. – DrPepper

0

Tuttavia, la scrittura di un metodo che avrebbe tentato di stampare il rapporto di ogni dipendente nel roster sarebbe impossibile, senza memorizzare in modo esplicito l'oggetto rm che viene inviata al reportAndAdd!

Non sono sicuro del tuo problema preciso. Il seguente dovrebbe funzionare (ovviamente con distinte relazioni concatenati al punto di uscita I/O):

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

Tuttavia, facendo I/O da qualche parte lungo il metodo-call-albero (o in metodi chiamati in modo iterativo) è contro la 'filosofia funzionale'. È meglio generare singoli sottoriport come String/List [String]/altra struttura precisa, aggiungerli a tutti i metodi più esterni e eseguire I/O in un singolo colpo. Es .:

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

(oggetti inserto implicite simili a 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"""))) 
} 
Problemi correlati