鍍金池/ 教程/ Scala/ 類型類
高階函數(shù)與 DRY
序列提取
譯者結(jié)語
類型 Future
類型 Option
Scala 初學指南
類型類
模式匹配與匿名函數(shù)
路徑依賴類型
提取器
類型 Either
Try 與錯誤處理
介紹
實戰(zhàn)中的 Promise 和 Future
柯里化和部分函數(shù)應用
無處不在的模式

類型類

前兩章討論了幾種保持 DRY 和靈活性的函數(shù)式編程技術(shù):

  1. 函數(shù)組合(function composition)
  2. 部分函數(shù)應用(partial function application)
  3. 柯里化(currying)

這一章依舊圍繞代碼靈活性而來,不過不再討論作為頭等公民的函數(shù),而是類型系統(tǒng)(注意:并不是要真的去研究類型系統(tǒng))。 你將學習 類型類 !

可能你會覺得這沒有實際意義,認為這是被 Haskell 狂熱分子帶入 Scala 社區(qū)的異國情調(diào),顯然不是這樣。 類型類已經(jīng)成為 Scala 標準庫,甚至是很多流行的、廣泛使用的第三方開源庫的重要組成部分,了解和熟悉類型類是很有必要的。

本章會討論:

  1. 類型類的概念,
  2. 它為什么有用,
  3. 使用它如何受益,
  4. 如何實現(xiàn)類型類,并用于實踐。

問題

我們用例子,而不是一個對類型類的抽象解釋,開始本文的主題,例子簡化了概念,也相當實用。

假設想提供一系列可以操作數(shù)字集合的函數(shù),主要是計算它們的聚合值。 進一步假設只能通過索引來訪問集合的元素,只能使用定義在 Scala 集合上的 reduce 方法。 (施加這些限制,是因為要實現(xiàn)的東西,Scala 標準庫已經(jīng)提供了) 最后,假定得到的值已排序。

我們先從 median , quartilesiqr 的一個粗暴實現(xiàn)開始:

    object Statistics {
      def median(xs: Vector[Double]): Double = xs(xs.size / 2)
      def quartiles(xs: Vector[Double]): (Double, Double, Double) =
        (xs(xs.size / 4), median(xs), xs(xs.size / 4 * 3))
      def iqr(xs: Vector[Double]): Double = quartiles(xs) match {
        case (lowerQuartile, _, upperQuartile) => upperQuartile - lowerQuartile
      }
      def mean(xs: Vector[Double]): Double = {
        xs.reduce(_ + _) / xs.size
      }
    }

median 將數(shù)據(jù)集分成兩半,下四分位數(shù)和上四分位數(shù)( quartiles 方法返回的元組的第一、第三個元素)分別分割了數(shù)據(jù)集的 25% 。 iqr 方法返回四分差(上四分衛(wèi)數(shù)和下四分位數(shù)的差)。

現(xiàn)在我們想支持更多的類型,比如,Int,所以應該為這個類型實現(xiàn)上面這些方法,對吧?

不!不能想當然的為 Vector[Int] 重載上面的方法(詭異的技巧除外),因為類型參數(shù)會被擦除,而且這樣做有代碼冗余的嫌疑。

要是 IntDouble 擴展自一個共同的基類,或者都實現(xiàn)了一個像是 Number 這樣的特質(zhì),那該多好!

你可能會想著去把上述方法需要的參數(shù)類型替換成更通用的類型,看起來會是這樣:

    object Statistics {
      def median(xs: Vector[Number]): Number = ???
      def quartiles(xs: Vector[Number]): (Number, Number, Number) = ???
      def iqr(xs: Vector[Number]): Number = ???
      def mean(xs: Vector[Number]): Number = ???
    }

這樣做,不僅丟掉了先前的類型信息,還違背了擴展性:不能強制第三方的數(shù)字類型擴展 Number 特質(zhì)。 幸運的是,本例并不存在這樣一個通用的特質(zhì)。

對于這種問題,Ruby 的做法是 猴子補?。╩onkey patching) ,擴展新類型讓它看起來像一個 Number ,但是這樣會污染全局命名空間。 年輕時遭到 “四人幫” 打擊的 Java 開發(fā)者,則會認為 適配器(Adpater) 能解決上面所有問題:

“四人幫”這里指的是設計模式一書的作者:Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides, 具體見:http://en.wikipedia.org/wiki/Design_Patterns

    object Statistics {
      trait NumberLike[A] {
        def get: A
        def plus(y: NumberLike[A]): NumberLike[A]
        def minus(y: NumberLike[A]): NumberLike[A]
        def divide(y: Int): NumberLike[A]
      }
      case class NumberLikeDouble(x: Double) extends NumberLike[Double] {
        def get: Double = x
        def minus(y: NumberLike[Double]) = NumberLikeDouble(x - y.get)
        def plus(y: NumberLike[Double]) = NumberLikeDouble(x + y.get)
        def divide(y: Int) = NumberLikeDouble(x / y)
      }
      type Quartile[A] = (NumberLike[A], NumberLike[A], NumberLike[A])
      def median[A](xs: Vector[NumberLike[A]]): NumberLike[A] = xs(xs.size / 2)
      def quartiles[A](xs: Vector[NumberLike[A]]): Quartile[A] =
        (xs(xs.size / 4), median(xs), xs(xs.size / 4 * 3))
      def iqr[A](xs: Vector[NumberLike[A]]): NumberLike[A] = quartiles(xs) match {
        case (lowerQuartile, _, upperQuartile) => upperQuartile.minus(lowerQuartile)
      }
      def mean[A](xs: Vector[NumberLike[A]]): NumberLike[A] =
        xs.reduce(_.plus(_)).divide(xs.size)
    }

上述代碼解決了擴展性問題:使用這個庫的用戶可以將類型通過 NumberLike 適配器傳遞過來,無需重新編譯統(tǒng)計庫。

但是,把數(shù)字封裝在適配器里,這樣的代碼會令人厭倦,無論讀寫,而且和統(tǒng)計庫交互時,必須創(chuàng)建一大堆適配器實例。

類型類來救援

對目前所介紹的方法來說,類型類是一個強大的替代。 類型類是 Haskell 語言一個突出的特征,雖然它的名字里有類,但它和面向?qū)ο缶幊汤锏念悰]有任何關(guān)系。

一個類型類 C 定義了一些行為,要想成為 C 的一員,類型 T 必須支持這些行為。 一個類型 T 到底是不是 類型類 C 的成員,這一點并不是與生俱來的。 開發(fā)者可以實現(xiàn)類必須支持的行為,使得這個類變成類型類的成員。 一旦 T 變成 類型類 C 的一員,參數(shù)類型為類型類 C 成員的函數(shù)就可以接受類型 T 的實例。

這樣,類型類支持臨時的、追溯性的多態(tài),依賴類型類的代碼支持擴展性,且無需創(chuàng)建任何適配器對象。

創(chuàng)建類型類

Scala 中,類型類可以通過技術(shù)組合來實現(xiàn)和使用,比之 Haskell,它在 Scala 里的參與度更高,而且?guī)Ыo開發(fā)者更多的控制。

創(chuàng)建一個類型類涉及到幾個步驟。

首先,我們來定義一個特質(zhì):

    object Math {
      trait NumberLike[T] {
        def plus(x: T, y: T): T
        def divide(x: T, y: Int): T
        def minus(x: T, y: T): T
      }
    }

上述代碼創(chuàng)建了名為 NumberLike 的類型類特質(zhì)。 類型類總會帶著一個或多個類型參數(shù),通常是無狀態(tài)的,比如:里面定義的方法只對傳入的參數(shù)進行操作。 前文的適配器操作的是它自己的字段和接受的一個參數(shù),而這里定義的方法都需要兩個參數(shù),其中第一個參數(shù)對應適配器中的字段。

提供默認成員

第二步通常是在伴生對象里提供一些默認的類型類特質(zhì)實現(xiàn),之后你會知道為什么要這么做。 在這之前,先來實現(xiàn) DoubleInt 的類型類特質(zhì):

    object Math {
      trait NumberLike[T] {
        def plus(x: T, y: T): T
        def divide(x: T, y: Int): T
        def minus(x: T, y: T): T
      }
      object NumberLike {
        implicit object NumberLikeDouble extends NumberLike[Double] {
          def plus(x: Double, y: Double): Double = x + y
          def divide(x: Double, y: Int): Double = x / y
          def minus(x: Double, y: Double): Double = x - y
        }
        implicit object NumberLikeInt extends NumberLike[Int] {
          def plus(x: Int, y: Int): Int = x + y
          def divide(x: Int, y: Int): Int = x / y
          def minus(x: Int, y: Int): Int = x - y
        }
      }
    }

兩件事情: 第一,這兩個實現(xiàn)基本相同。但不總是這樣,畢竟 NumberLike 只是一個很小的域。 后面會給出類型類的一些例子,當為這些例子實現(xiàn)多個類型時,重復的余地就少很多。 第二, NumberLikeInt 做整數(shù)除法的時候,會損失一些精度,請忽略這一事實,這只是為簡單起見。

你也許會發(fā)現(xiàn),類型類的成員通常是單例對象,而且會有一個 implicit 關(guān)鍵字位于前面, 這是類型類在 Scala 中成為可能的幾個重要因素之一,在某些條件下,它讓類型類成員隱式可用。 更多相關(guān)的知識在下一節(jié)。

運用類型類

有了類型類和兩個默認實現(xiàn)之后,就可以根據(jù)它們來實現(xiàn)統(tǒng)計。 我們先將重點放在 mean 方法上:

    object Statistics {
      import Math.NumberLike
      def mean[T](xs: Vector[T])(implicit ev: NumberLike[T]): T =
        ev.divide(xs.reduce(ev.plus(_, _)), xs.size)
    }

這樣的代碼初看起來可能有點嚇人,實際上是相當簡單,方法帶有一個類型參數(shù) T ,接受類型為 Vector[T] 的參數(shù)。

將參數(shù)限制在特定類型類的成員上,是通過第二個 implicit 參數(shù)列表實現(xiàn)的。 這是什么意思?這是說,當前作用域中必須存在一個隱式可用的 NumberLike[T] 對象,比如說,當前作用域聲明了一個 隱式值(implicit value)。 這種聲明很多時候都是通過導入一個有隱式值定義的包或者對象來實現(xiàn)的。

當且僅當沒有發(fā)現(xiàn)其他隱式值時,編譯器會在隱式參數(shù)類型的伴生對象中尋找。 作為庫的設計者,將默認的類型類實現(xiàn)放在伴生對象里意味著庫的使用者可以輕易的重寫默認實現(xiàn),這正是庫設計者喜聞樂見的。 用戶還可以為隱式參數(shù)傳遞一個顯示值,來重寫作用域內(nèi)的隱式值。

讓我們來驗證下默認的實現(xiàn)是否可以被正確解析:

    val numbers = Vector[Double](13, 23.0, 42, 45, 61, 73, 96, 100, 199, 420, 900, 3839)
    println(Statistics.mean(numbers))

漂亮極了!試試 Vector[String] ,你會在編譯期得到一個錯誤,這個錯誤指出參數(shù) ev: NumberLike[String] 沒有隱式值可用。 如果你不喜歡這個錯誤消息,你可以用 @implicitNotFound 為類型類添加批注,來自定義錯誤消息:

    object Math {
      import annotation.implicitNotFound
      @implicitNotFound("No member of type class NumberLike in scope for ${T}")
      trait NumberLike[T] {
        def plus(x: T, y: T): T
        def divide(x: T, y: Int): T
        def minus(x: T, y: T): T
      }
    }

上下文綁定

總是帶著這個隱式參數(shù)列表顯得有些冗長。 對于只有一個類型參數(shù)的隱式參數(shù),Scala 提供了一種叫做 上下文綁定(context bound) 的簡寫。 為了說明這一使用方法,我們用它來實現(xiàn)剩下的統(tǒng)計方法:

    object Statistics {
      import Math.NumberLike
      def mean[T](xs: Vector[T])(implicit ev: NumberLike[T]): T =
        ev.divide(xs.reduce(ev.plus(_, _)), xs.size)
      def median[T : NumberLike](xs: Vector[T]): T = xs(xs.size / 2)
      def quartiles[T: NumberLike](xs: Vector[T]): (T, T, T) =
        (xs(xs.size / 4), median(xs), xs(xs.size / 4 * 3))
      def iqr[T: NumberLike](xs: Vector[T]): T = quartiles(xs) match {
        case (lowerQuartile, _, upperQuartile) =>
          implicitly[NumberLike[T]].minus(upperQuartile, lowerQuartile)
      }
    }

上下文綁定 T: NumberLike 意思是,必須有一個類型為 NumberLike[T] 的隱式值在當前上下文中可用,這和隱式參數(shù)列表是等價的。 如果想要訪問這個隱式值,需要調(diào)用 implicitly 方法,就像上述 iqr 方法所做的那樣。 如果類型類需要多個類型參數(shù),就不能使用上下文綁定語法了。

自定義的類型類成員

含有類型類的庫的使用者,或遲或早會想將他自己的類型加入到類型類成員中。 比如說,可能想將統(tǒng)計用在 Joda Time 的 Duration 實例上。

我們來試試吧。首先將 Joda Time 加入到路徑里:

    libraryDependencies += "joda-time" % "joda-time" % "2.1"

    libraryDependencies += "org.joda" % "joda-convert" % "1.3"

現(xiàn)在,只需創(chuàng)建 NumberLike 的一個隱式實現(xiàn):

    object JodaImplicits {
      import Math.NumberLike
      import org.joda.time.Duration
      implicit object NumberLikeDuration extends NumberLike[Duration] {
        def plus(x: Duration, y: Duration): Duration = x.plus(y)
        def divide(x: Duration, y: Int): Duration = Duration.millis(x.getMillis / y)
        def minus(x: Duration, y: Duration): Duration = x.minus(y)
      }
    }

導入包含這個實現(xiàn)的包或者對象,就可以計算一堆 durations 的平均值了:

    import Statistics._
    import JodaImplicits._
    import org.joda.time.Duration._

    val durations = Vector(standardSeconds(20), standardSeconds(57), standardMinutes(2),
      standardMinutes(17), standardMinutes(30), standardMinutes(58), standardHours(2),
      standardHours(5), standardHours(8), standardHours(17), standardDays(1),
      standardDays(4))
    println(mean(durations).getStandardHours)

使用場景

NumberLike 類型類是一個非常好的例子,但 Scala 已經(jīng)有 Numeric 了。 對于集合的類型參數(shù) T ,只要存在一個可用的 Numeric[T],就可以在該集合上調(diào)用 sum 、 product 這樣的方法。 標準庫中另一個使用比較多的類型類是 Ordering,可以為自定義類型提供一個隱式排序,用在 Scala 集合的 sort 方法。

標準庫中還有更多這樣的類型類,不過,Scala 開發(fā)者并不需要與它們中的每一個都打交道。

第三方庫中一個非常常見的用例是對象序列化和反序列化,尤其是 JSON 對象。 使一個類成為某個格式器類型類的成員,就可以自定義類的序列化方式,序列化成 JSON、XML 或者是任何新的格式。

Scala 類型和數(shù)據(jù)庫驅(qū)動支持的類型之間的映射,通常也是通過類型類獲得自定義和可擴展性的。

總結(jié)

一旦開始用 Scala 來做些正式的工作,不可避免的會遇到類型類。 希望讀者在讀完這一章后,能夠利用好這一強大技術(shù)。

Scala 類型類使得在開發(fā) Scala 應用時,一方面可以有無限可追加的擴展, 另一方面又可以保留盡可能多的具體類型信息。

和其他語言應對這種問題的方法想比,Scala 給予了開發(fā)者完全的控制權(quán),因為類型類的實現(xiàn)可以被輕易的重寫,而且在全局命名空間里不可用。

你將看到這種技術(shù)在編寫由其他人使用的庫時尤其有用,在應用程序代碼中,為了減少模塊之間的耦合,類型類也是有用武之地的。

上一篇:無處不在的模式下一篇:提取器