本文由 Yison 發表在 ScalaCool 團隊部落格。
Scala 中的 collection 庫是符合 DRY 設計原則的典範,它包含了大量通用的集合操作 API,由此我們可以基於標準庫,輕鬆構建出一個強大的新集合型別。
本文將介紹「如何實現一個新集合類」,在開始之前,我們先來了解下 Scala 2.8 版本後的集合結構設計。
集合通用設計
看過 Scala 中的集合(一) 的朋友已經知道,Scala 的集合類系統地區分了可變的和不可變的集合,它們存在於以下三個包中:
- scala.collection
- scala.collection.mutable
- scala.collection.immutable
然而,以上所有的集合都繼承了兩個相同的特質 — Traversable
和 Iterable
(後者繼承了前者)。
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 出錯- 遍歷一個
Traversable
比Iterable
效能要差 - 所有繼承了
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)複製程式碼
這下有難度了,僅憑 Builder
和 foreach
組合,似乎完成不了這個任務。
於是,我們決定看下 TraversableLike
中 map
的 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
的通用集合方法,如length
、last
- 但
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 版本中被新的方案替代。