While working on a Scala project that used the Type Class pattern, I ran into what appears to be a serious problem in how the language implements the pattern: Since Scala type-class implementations must be managed by the programmer and not the language, any variable belonging to a type-class can never become annotated as a parent type unless its type-class implementation is taken with it.
To illustrate this point, I've coded up a quick example program. Imagine you were trying to write a program that could handle different kinds of employees for a company and could print reports on their progress. To solve this with the type-class pattern in Scala, you might try something like this:
abstract class Employee
class Packer(boxesPacked: Int, cratesPacked: Int) extends Employee
class Shipper(trucksShipped: Int) extends Employee
A class hierarchy modelling different kinds of employees, simple enough. Now we implement the ReportMaker type-class.
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) }
}
That's all well and good, and we can now write some kind of Roster class that might look like this:
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 this works. Now, thanks to our type-class, we can pass either a packer or a shipper object into the reportAndAdd method, and it will print the report and add the employee to the roster. However, writing a method that would attempt to print out the report of every employee in the roster would be impossible, without explicitly storing the rm object that gets passed to reportAndAdd!
Two other languages that support the pattern, Haskell and Clojure, don't share this problem, since they deal with this problem. Haskell's stores the mapping from datatype to implementation globally, so it is always 'with' the variable, and Clojure basically does the same thing. Here's a quick example that works perfectly 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)
Apart from the rather nasty solution of turning the employee list into type List[(Employee, ReportMaker[Employee]), does Scala offer any way to solve this issue? And if not, since the Scala libraries make extensive use of Type-Classes, why hasn't it been addressed?
The way you'd typically implement an algebraic data type in Scala would be with case
classes:
sealed trait Employee
case class Packer(boxesPacked: Int, cratesPacked: Int) extends Employee
case class Shipper(trucksShipped: Int) extends Employee
This gives pattern extractors for the Packer
and Shipper
constructors, so you can match on them.
Unfortunately, Packer
and Shipper
are also distinct (sub)types, but part of the pattern of encoding an algebraic data type in Scala is to be disciplined about ignoring this. Instead, when distinguishing between a packer or shipper, use pattern matching as you would in Haskell:
implicit object EmployeeReportMaker extends ReportMaker[Employee] {
def printReport(e: Employee) = e match {
case Packer(boxes, crates) => // ...
case Shipper(trucks) => // ...
}
}
If you have no other types for which you need a ReportMaker
instance, then perhaps the type class isn't needed, and you can just use the printReport
function.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With