Scala 中的集合(三):實現一個新的 Collection 類

ScalaCool發表於2017-07-29

本文由 Yison 發表在 ScalaCool 團隊部落格。

Scala 中的 collection 庫是符合 DRY 設計原則的典範,它包含了大量通用的集合操作 API,由此我們可以基於標準庫,輕鬆構建出一個強大的新集合型別。

本文將介紹「如何實現一個新集合類」,在開始之前,我們先來了解下 Scala 2.8 版本後的集合結構設計。

集合通用設計

看過 Scala 中的集合(一) 的朋友已經知道,Scala 的集合類系統地區分了可變的和不可變的集合,它們存在於以下三個包中:

  • scala.collection
  • scala.collection.mutable
  • scala.collection.immutable

然而,以上所有的集合都繼承了兩個相同的特質 — TraversableIterable(後者繼承了前者)。

Traversable

Traversable 是集合類最高階的特性,它具有一個抽象方法:

def foreach[U](f: Elem => U)複製程式碼

顧名思義,foreach 方法用於遍歷集合類的所有元素,然後進行指定的操作。Iterable 繼承了 Traversable,也實現了 foreach 方法,繼而所有繼承了 Iterable 的集合類同時也獲得了一個 foreach 的基礎版本。

很多集合操作都是基於 foreach 實現,因此它的效能非常關鍵。一些 Iterable 子類覆寫了這個方法的實現,從而獲得了符合不同集合特性的優化。

那麼,常見的集合型別(如 Seq) 是如何實現通用操作的呢(如 map)?

原來,Traversable 除了唯一的抽象方法以外,還包含了大量通用的集合操作方法。

Scala 文件對這些操作方法進行了歸類,如下所示:

分類 方法
抽象方法 foreach
相加 ++
Map map / flatMap / collect
集合轉換 toArray / toList / toIterable / toSeq / toIndexedSeq / toStream / toSet / toMap
拷貝 copyToBuffer / copyToArray
size 資訊 isEmpty / nonEmpty / size / hasDefiniteSize
元素檢索 head / last / headOption / lastOption / find
子集合檢索 tail / init / slice / take / drop / takeWhilte / dropWhile / filter / filteNot / withFilter
拆分 splitAt / span / partition / groupBy
元素測試 exists / forall / count
摺疊 foldLeft / foldRight / /: / :\ / reduceLeft / reduceRight
特殊摺疊 sum / product / min / max
字串轉化 mkString / addString / stringPrefix
檢視生成 view

由此,一個集合僅需定義 foreach 方法,以上所有其它方法都可以從 Traversable 繼承。

Iterable

Scala 當前版本的 Iterable 設計略顯尷尬,它實現了 Traversable,也同時被其它所有集合實現。然而事實上這並不是一個好的設計,原因如下:

  • Traversable 具有隱式的行為假設,它在公開的簽名中是不可見的,容易導致 API 出錯
  • 遍歷一個 TraversableIterable 效能要差
  • 所有繼承了 Traversable 的資料型別,無不接受 Iterator 的實現,前者顯得多餘

詳情參見 @Alexelcu 的文章 — Why scala.collection.Traversable Is Bad Design

因此,正在進行的 Scala collection redesign 專案也已經拋棄了 Traversable

然而,這並不妨礙我們研究 Iterable 中的通用方法,它們也在 collection-strawman 中被保留,如下所示:

分類 方法
抽象方法 iterator
其他迭代器 grouped / sliding
子集合 takeRight / dropRight
拉鍊操作 zip / zipAll
比對 sameElements

Builder 類

幾乎所有的集合操作都由「遍歷器」和「構建器」完成,在瞭解以上內容之後,我們再來了解下如何構建一個集合型別。在當前的 Scala 中,是利用一個 Builder 類實現的。

package scala.collection.mutable
class Builder[-Elem, +To] {
  def +=(elem: Elem): this.type
  def result(): To
  def clear(): Unit
  def mapResult[NewTo](f: To => NewTo): Builder[Elem, NewTo] = ...
}複製程式碼

注意型別引數,Elem 表示元素的型別(如 Int ),To 表示集合的型別(如 Array[Int])。

此外:

  • += 可以增加元素
  • result 返回一個集合
  • clear 把集合重置為空狀態
  • mapResult 返回一個 Builder,擁有新的集合型別

我們來看下Builder 如何結合 foreach 方法,實現常見的 filter 操作:

def filter(p: Elem => Boolean): Repr = {
  val b = newBuilder
  foreach { elem => if (p(elem)) b += elem }
  b.result
}複製程式碼

So easy!沒什麼挑戰。

我們再來考慮下 map,它與 filter 的差異之一,在於前者可以返回一個「元素型別不同」的集合。如:

scala > List(1, 2, 3).map(_.toString)
res0: List[String] = List(1, 2, 3)複製程式碼

這下有難度了,僅憑 Builderforeach 組合,似乎完成不了這個任務。

於是,我們決定看下 TraversableLikemap 的 Scala 原始碼實現:

def map[B, That](f: Elem => B)
    (implicit bf: CanBuildFrom[Repr, B, That]): That = {
  val b = bf(this)
  this.foreach(x => b += f(x))
  b.result
}複製程式碼

當前 Scala 集合中,???Like 命名的特質是 ??? 特質的實現。

一個大發現 — 當前版本的 Scala 原來是利用 CanBuildFrom 型別來解決如何集合「型別轉換」的問題

package scala.collection.generic
trait CanBuildFrom[-From, -Elem, +To] {
  // 建立一個新的構造器(builder)
  def apply(from: From): Builder[Elem, To]
}複製程式碼

這種利用 TypeClass 技術 — 採用隱式轉換來獲得擴充套件的方式,顯得強大且靈活,但在新手看來會比較怵。

通過字面的理解,我們知曉 — From 代表當前的集合型別,Elem 代表元素型別,To 代表目標集合的型別。
所以我們可以如此解讀 CanBuildFrom:「有這麼一個方法,由給定的 From 型別的集合,使用 Elem 型別,建立 To 型別的集合」。

新集合類實現

通過以上的介紹,大家對 Scala 的集合結構設計有了整體的認識,現在開始來實現一個新的集合類。

以下例子來自 Scala 文件,細節有調整,精簡。

假設我們需要設計一套新的「密文編碼序列」,由最基本的 A、B、C、D 四個字母組成。定義型別如下:

abstract class Base
case object A extends Base
case object B extends Base
case object C extends Base
case object D extends Base
object Base {
  val fromInt: Int => Base = Array(A, B, C, D)
  val toInt: Base => Int = Map(A -> 0, B -> 1, C -> 2, D -> 3)
}複製程式碼

顯然,我們可以使用 Seq[Base] 來表示一個密文序列,但由於這個密文可能很長,並且 Base 型別只有 4 種可能,我們可以通過「位計算」的方式來開發一種壓縮過的集合,它是 Seq[Base] 的子類。

以下將採用伴生物件的方式來建立 Message 例項,可參考 Builder 建立者模式

import collection.IndexedSeqLike

final class Message private (
    val groups: Array[Int],
    val length: Int) extends IndexedSeq[Base] {
  import Message._
  def apply(idx: Int): Base = {
    if (idx < 0 || length <= idx)
      throw new IndexOutOfBoundsException
    Base.fromInt(groups(idx / N) >> (idx % N * S) & M)
  }
}

object Message {
  private val S = 2 // 表示一組所需要的位數             
  private val N = 32 / S  // 一個Int能夠放入的組數      
  private val M = (1 << S) - 1 // 分離組的位掩碼(bitmask)
  def fromSeq(buf: Seq[Base]): Message = {
    val groups = new Array[Int]((buf.length + N - 1) / N)
    for (i <- 0 until buf.length)
      groups(i / N) |= Base.toInt(buf(i)) << (i % N * S)
    new Message(groups, buf.length)
  }
  def apply(bases: Base*) = fromSeq(bases)
}複製程式碼

測試:

val message = Message(A, B, B ,D)
println(message.length) // 4
println(message.last) // D
println(message.take(3)) // Vector(A, B, B)複製程式碼
  • Message 很好地獲得了 IndexedSeq 的通用集合方法,如 lengthlast
  • take 方法並沒有獲得預期的 Message(A, B, B),而是 Vector(A, B, B)

改進一下:

def take(count: Int): Message = Message.fromSeq(super.take(count))複製程式碼
  • 確實可以解決 take 返回動態型別的問題,可得到 Message(A, B, B)的結果
  • 然而集合除了 take 外還有大量通用方法,覆寫每個方法的策略不可取

正確的姿勢

import collection.mutable.{Builder, ArrayBuffer}
import collection.generic.CanBuildFrom複製程式碼

在伴生類中重新實現 newBuilder

final class Message private (val groups: Array[Int], val length: Int)
  extends IndexedSeq[Base] with IndexedSeqLike[Base, Message] {
  import Message._

  // 在IndexedSeq中必須重新實現newBuilder
  override protected[this] def newBuilder: Builder[Base, Message] =
    Message.newBuilder

  def apply(idx: Int): Base = {
    ……
  }

}複製程式碼

改寫伴生物件:

object Message {
  ……
  def fromSeq(buf: Seq[Base]): Message = {
    ……
  }

  def apply(bases: Base*) = fromSeq(bases)

  def newBuilder: Builder[Base, Message] =
    new ArrayBuffer mapResult fromSeq

  implicit def canBuildFrom: CanBuildFrom[Message, Base, Message] =
    new CanBuildFrom[Message, Base, Message] {
      def apply(): Builder[Base, Message] = newBuilder
      def apply(from: Message): Builder[Base, Message] = newBuilder
    }
}複製程式碼

此外,如前文提到,我們還可以重新實現 foreach 方法來提高該集合類的效率:

final class Message private (val groups: Array[Int], val length: Int)
  extends IndexedSeq[Base] with IndexedSeqLike[Base, Message] {
  ……
  override def foreach[U](f: Base => U): Unit = {
    var i = 0
    var b = 0
    while (i < length) {
      b = if (i % N == 0) groups(i / N) else b >>> S
      f(Base.fromInt(b & M))
      i += 1
    }
  }
}複製程式碼

以上,我們便構建了一個新的集合型別 Message,通過極少的程式碼,擁有了強大的通用集合特性。

我們將在下一篇文章中進一步介紹 CanBuildFrom ,幾乎確定地說,它也會在未來的 Scala 版本中被新的方案替代。

參考

相關文章