在处理使用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,为什么还没有解决它呢?
答案 0 :(得分:5)
您通常在Scala中实现代数数据类型的方式是使用case
类:
sealed trait Employee
case class Packer(boxesPacked: Int, cratesPacked: Int) extends Employee
case class Shipper(trucksShipped: Int) extends Employee
这为Packer
和Shipper
构造函数提供了模式提取器,因此您可以匹配它们。
不幸的是,Packer
和Shipper
也是不同的(子)类型,但是在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""")))
}