2013-08-16 123 views
6

在使用Type类模式的Scala项目中工作时,我遇到了语言如何实现该模式的严重问题:由于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) } 
} 

这一切都很好,我们现在可以写一些类型的名册类的看起来像这样的:

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

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

所以此工程。现在,由于我们的类型,我们可以将一个打包器或托运对象传递给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) 

除了开启员工列表为类型列表中,而讨厌方案(员工,ReportMaker [雇员]),并提供斯卡拉任何方式来解决这个问题?如果不是,由于Scala库大量使用类型类,为什么它没有得到解决?

+2

这又是的亚型搞乱了一切另一个例子。如果你认为你的员工子类是构造函数(从Haskell的意义上)而不是子类型,你会发现类型类方法更舒适。 –

+3

我不确定Haskell会如何解决这个问题。 Haskell的标准列表类型不是异构的,所以所有元素都会有相同的类型实例。将不同类型的“Packer”和“Shipper”放在同一个列表中的想法是行不通的。 –

回答

5

你通常会实现斯卡拉代数数据类型的方式将与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函数。

+0

这并不能解决问题,因为正如你所说的那样,只有在知道某人稍后需要添加更多数据类型时才会使用类型类。例如,员工代码的作者可能会认为某个地方稍后会有人需要添加一个Manager类。但是,由于您使用模式匹配来解决问题,因此除非他自己修改库代码,否则不能添加该类。他可能无法做到。 – DrPepper

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