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