DrPepper
DrPepper

Reputation: 237

Problems generalizing Scala type classes

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?

Upvotes: 6

Views: 667

Answers (2)

Glen Best
Glen Best

Reputation: 23105

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!

Not sure of your precise problem. The following should work (obviously with separate reports concatenated at the I/O output point):

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

However, doing I/O somewhere down the method-call-tree (or in methods called iteratively) is against the 'functional philosophy'. Better to generate individual sub-reports as String / List[String] / other precise structure, bubble them all the up to the outermost method and do I/O in a single hit. E.g.:

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

(insert implicit Objects similar to 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""")))
}

Upvotes: 0

Ben James
Ben James

Reputation: 125157

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.

Upvotes: 5

Related Questions