2013-08-16 49 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"""))) 
}