6

在处理使用类型类模式的 Scala 项目时,我遇到了语言如何实现该模式的一个严重问题:由于 Scala 类型类实现必须由程序员而不是语言管理,因此任何变量属于一个类型类永远不会被注释为父类型,除非它的类型类实现被带走。

为了说明这一点,我编写了一个快速示例程序。想象一下,您正在尝试编写一个程序,该程序可以为公司处理不同类型的员工,并可以打印有关他们进度的报告。要使用 Scala 中的类型类模式解决此问题,您可以尝试以下方法:

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

对不同类型的员工进行建模的类层次结构,非常简单。现在我们实现 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) }
}

这一切都很好,我们现在可以编写某种可能如下所示的 Roster 类:

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

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

所以这行得通。现在,由于我们的 type-class,我们可以将 packer 或 shipper 对象传递给 reportAndAdd 方法,它将打印报告并将员工添加到花名册中。但是,如果不显式存储传递给 reportAndAdd 的 rm 对象,编写一个尝试打印出名册中每个员工的报告的方法是不可能的!

支持该模式的另外两种语言 Haskell 和 Clojure 不存在这个问题,因为它们处理这个问题。Haskell 全局存储从数据类型到实现的映射,因此它始终与变量“同”,而 Clojure 基本上做同样的事情。这是一个在 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)

除了将员工列表转换为 List[(Employee, ReportMaker[Employee]) 类型的相当讨厌的解决方案之外,Scala 是否提供任何解决此问题的方法?如果没有,既然 Scala 库广泛使用类型类,为什么没有解决它?

4

2 回答 2

5

您通常在 Scala 中实现代数数据类型的方式是使用case类:

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

这为PackerShipper构造函数提供了模式提取器,因此您可以匹配它们。

不幸的是,PackerandShipper也是不同的(子)类型,但是在 Scala 中编码代数数据类型的模式的一部分是要注意忽略这一点。相反,在区分打包程序或托运人时,请像在 Haskell 中一样使用模式匹配:

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

如果您没有需要ReportMaker实例的其他类型,则可能不需要类型类,您可以使用该printReport函数。

于 2013-08-16T09:09:10.993 回答
0

但是,如果不显式存储传递给 reportAndAdd 的 rm 对象,编写一个尝试打印出名册中每个员工的报告的方法是不可能的!

不确定您的确切问题。以下应该可以工作(显然在 I/O 输出点连接单独的报告):

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

然而,在方法调用树的某个地方(或在迭代调用的方法中)进行 I/O 是违反“功能哲学”的。最好将单独的子报告生成为 String / List[String] / 其他精确结构,将它们全部冒泡到最外层的方法并一次执行 I/O。例如:

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

(插入类似于 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""")))
}
于 2013-12-12T00:57:46.760 回答