概括Scala类型的问题

时间:2013-08-16 00:23:29

标签: scala typeclass

在处理使用Type Class模式的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
    }
}

所以这很有效。现在,由于我们的类型类,我们可以将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库大量使用Type-Classes,为什么还没有解决它呢?

2 个答案:

答案 0 :(得分:5)

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

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

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

不幸的是,PackerShipper也是不同的(子)类型,但是在Scala中编码代数数据类型的模式的一部分是要遵守忽略它的规则。相反,在区分打包器或出货单时,请使用模式匹配,就像在Haskell中一样:

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

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

答案 1 :(得分: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. E.g:

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""")))
}