Julien Richard-Foy
本指南說明如何撰寫可套用至任何集合類型並傳回相同集合類型的操作,以及如何撰寫可透過要建置的集合類型參數化的操作。建議先閱讀有關 集合架構 的文章。
下列各節說明如何使用、產生和轉換任何集合類型。
使用任何集合
在第一節中,我們展示如何撰寫使用任何集合實例的方法,該實例是集合階層的一部分。我們在第二節中展示如何支援類集合類型,例如 String
和 Array
(它們不延伸 IterableOnce
)。
使用任何實際集合
讓我們從最簡單的案例開始:使用任何集合。您不需要知道集合的精確類型,但只需知道它是一個集合。這可透過將 IterableOnce[A]
作為參數,或將 Iterable[A]
作為參數(如果您需要多次遍歷)來達成。
例如,假設我們要實作一個 sumBy
作業,在集合的元素經過函數轉換後對其求和
case class User(name: String, age: Int)
val users = Seq(User("Alice", 22), User("Bob", 20))
println(users.sumBy(_.age)) // “42”
我們可以使用隱式類別將 sumBy
作業定義為延伸方法,以便可以像呼叫方法一樣呼叫它
import scala.collection.IterableOnce
implicit class SumByOperation[A](coll: IterableOnce[A]) {
def sumBy[B](f: A => B)(implicit num: Numeric[B]): B = {
val it = coll.iterator
var result = f(it.next())
while (it.hasNext) {
result = num.plus(result, f(it.next()))
}
result
}
}
很遺憾的是,這個延伸方法無法使用 String
類型的值,甚至無法使用 Array
。這是因為這些類型不屬於 Scala 集合階層。不過,它們可以轉換為適當的集合類型,但延伸方法無法直接在 String
和 Array
上執行,因為這需要連續套用兩個隱式轉換。
我們可以將 sumBy
操作定義為一個擴充方法,這樣就可以像呼叫方法一樣呼叫它
import scala.collection.IterableOnce
extension [A](coll: IterableOnce[A])
def sumBy[B: Numeric](f: A => B): B =
val it = coll.iterator
var result = f(it.next())
while it.hasNext do
result = summon[Numeric[B]].plus(result, f(it.next()))
result
使用任何類似集合的類型
如果我們希望 sumBy
能夠在任何類似集合的類型上執行,例如 String
和 Array
,我們必須再增加一層間接層級
import scala.collection.generic.IsIterable
class SumByOperation[A](coll: IterableOnce[A]) {
def sumBy[B](f: A => B)(implicit num: Numeric[B]): B = ... // same as before
}
implicit def SumByOperation[Repr](coll: Repr)(implicit it: IsIterable[Repr]): SumByOperation[it.A] =
new SumByOperation[it.A](it(coll))
類型 IsIterable[Repr]
具有所有可以轉換為 IterableOps[A, Iterable, C]
的類型 Repr
的隱式實例(對於某些元素類型 A
和某些集合類型 C
)。實際集合類型和 String
和 Array
都有實例。
我們希望 sumBy
能夠在任何類似集合的類型上執行,例如 String
和 Array
。幸運的是,類型 IsIterable[Repr]
具有所有可以轉換為 IterableOps[A, Iterable, C]
的類型 Repr
的隱式實例(對於某些元素類型 A
和某些集合類型 C
),而且實際集合類型和 String
和 Array
都有實例。
import scala.collection.generic.IsIterable
extension [Repr](repr: Repr)(using iter: IsIterable[Repr])
def sumBy[B: Numeric](f: iter.A => B): B =
val coll = iter(repr)
... // same as before
使用比 Iterable
更具體的集合
在某些情況下,我們希望(或需要)運算式的接收者比 Iterable
更具體。例如,有些運算式僅對 Seq
有意義,但對 Set
沒有意義。
在這種情況下,同樣地,最直接的解決方案是將 Seq
作為參數,而不是 Iterable
或 IterableOnce
,但這僅適用於實際 Seq
值。如果您想支援 String
和 Array
值,您必須使用 IsSeq
。 IsSeq
類似於 IsIterable
,但提供轉換為 SeqOps[A, Iterable, C]
(對於某些類型 A
和 C
)。
使用 IsSeq
也需要讓您的運算式對 SeqView
值起作用,因為 SeqView
沒有延伸 Seq
。類似地,有一個 IsMap
類型,讓運算式同時對 Map
和 MapView
值起作用。
在這種情況下,同樣地,最直接的解決方案是將 Seq
作為參數,而不是 Iterable
或 IterableOnce
。類似於 IsIterable
,IsSeq
提供轉換為 SeqOps[A, Iterable, C]
(對於某些類型 A
和 C
)。
IsSeq
也讓你的操作在 SeqView
值上運作,因為 SeqView
沒有延伸 Seq
。類似地,有一個 IsMap
類型,讓操作同時在 Map
和 MapView
值上運作。
產生任何集合
這種情況發生在函式庫提供一個操作來產生集合,同時讓使用者選擇精確的集合類型。
例如,考慮一個類型類別 Gen[A]
,其實例定義如何產生類型 A
的值。此類型類別通常用於建立任意的測試資料。我們的目標是定義一個 collection
操作,用來產生包含任意值的任意集合。以下是一個 collection
的使用範例
scala> collection[List, Int].get
res0: List[Int] = List(606179450, -1479909815, 2107368132, 332900044, 1833159330, -406467525, 646515139, -575698977, -784473478, -1663770602)
scala> collection[LazyList, Boolean].get
res1: LazyList[Boolean] = LazyList(_, ?)
scala> collection[Set, Int].get
res2: Set[Int] = HashSet(-1775377531, -1376640531, -1009522404, 526943297, 1431886606, -1486861391)
Gen[A]
的一個非常基本的定義可能是以下
trait Gen[A] {
/** Get a generated value of type `A` */
def get: A
}
trait Gen[A]:
/** Get a generated value of type `A` */
def get: A
而且可以定義以下實例
import scala.util.Random
object Gen {
/** Generator of `Int` values */
implicit def int: Gen[Int] =
new Gen[Int] { def get: Int = Random.nextInt() }
/** Generator of `Boolean` values */
implicit def boolean: Gen[Boolean] =
new Gen[Boolean] { def get: Boolean = Random.nextBoolean() }
/** Given a generator of `A` values, provides a generator of `List[A]` values */
implicit def list[A](implicit genA: Gen[A]): Gen[List[A]] =
new Gen[List[A]] {
def get: List[A] =
if (Random.nextInt(100) < 10) Nil
else genA.get :: get
}
}
import scala.util.Random
object Gen:
/** Generator of `Int` values */
given Gen[Int] with
def get: Int = Random.nextInt()
/** Generator of `Boolean` values */
given Gen[Boolean] with
def get: Boolean = Random.nextBoolean()
/** Given a generator of `A` values, provides a generator of `List[A]` values */
given[A: Gen]: Gen[List[A]] with
def get: List[A] =
if Random.nextInt(100) < 10 then Nil
else summon[Gen[A]].get :: get
最後一個定義 (list
) 會產生一個類型為 List[A]
的值,給定一個類型為 A
的值的產生器。我們也可以實作一個 Vector[A]
或 Set[A]
的產生器,但它們的實作將會非常類似。
相反地,我們想抽象出已產生集合的類型,以便使用者可以決定要產生哪種類型的集合。
為了達成此目標,我們必須使用 scala.collection.Factory
trait Factory[-A, +C] {
/** @return A collection of type `C` containing the same elements
* as the source collection `it`.
* @param it Source collection
*/
def fromSpecific(it: IterableOnce[A]): C
/** Get a Builder for the collection. For non-strict collection
* types this will use an intermediate buffer.
* Building collections with `fromSpecific` is preferred
* because it can be lazy for lazy collections.
*/
def newBuilder: Builder[A, C]
}
trait Factory[-A, +C]:
/** @return A collection of type `C` containing the same elements
* as the source collection `it`.
* @param it Source collection
*/
def fromSpecific(it: IterableOnce[A]): C
/** Get a Builder for the collection. For non-strict collection
* types this will use an intermediate buffer.
* Building collections with `fromSpecific` is preferred
* because it can be lazy for lazy collections.
*/
def newBuilder: Builder[A, C]
end Factory
Factory[A, C]
特質提供了兩種建立集合 C
的方法,方法是使用類型 A
的元素
fromSpecific
,將A
的來源集合轉換為集合C
,newBuilder
,提供Builder[A, C]
。
這兩種方法的差異在於前者不一定會評估來源集合的元素。它可以產生非嚴格的集合類型(例如 LazyList
),除非它被遍歷,否則不會評估其元素。另一方面,基於建構函式的集合建構方法一定會評估結果集合的元素。實際上,建議 不要急於評估集合的元素。
最後,以下是我們如何實作任意集合類型的產生器
import scala.collection.Factory
implicit def collection[CC[_], A](implicit
genA: Gen[A],
factory: Factory[A, CC[A]]
): Gen[CC[A]] =
new Gen[CC[A]] {
def get: CC[A] = {
val lazyElements =
LazyList.unfold(()) { _ =>
if (Random.nextInt(100) < 10) None
else Some((genA.get, ()))
}
factory.fromSpecific(lazyElements)
}
}
import scala.collection.Factory
given[CC[_], A: Gen](using Factory[A, CC[A]]): Gen[CC[A]] with
def get: CC[A] =
val lazyElements =
LazyList.unfold(()) { _ =>
if Random.nextInt(100) < 10 then None
else Some((summon[Gen[A]].get, ()))
}
summon[Factory[A, CC[A]]].fromSpecific(lazyElements)
實作使用隨機大小的延遲來源集合(lazyElements
)。然後它呼叫 Factory
的 fromSpecific
方法,以建立使用者預期的集合。
轉換任何集合
轉換集合包含使用和產生集合。這是透過結合先前各節中所述的技巧來達成。
例如,我們想實作 intersperse
作業,它可以套用至任何序列,並傳回一個序列,其中在來源序列的每個元素之間插入一個新元素
List(1, 2, 3).intersperse(0) == List(1, 0, 2, 0, 3)
"foo".intersperse(' ') == "f o o"
當我們在 List
上呼叫它時,我們希望取得另一個 List
,當我們在 String
上呼叫它時,我們希望取得另一個 String
,以此類推。
根據我們從前一節所學到的內容,我們可以使用 IsSeq
開始定義一個擴充方法,並使用隱含的 Factory
產生一個集合
import scala.collection.{ AbstractIterator, AbstractView, Factory }
import scala.collection.generic.IsSeq
class IntersperseOperation[Repr](coll: Repr, seq: IsSeq[Repr]) {
def intersperse[B >: seq.A, That](sep: B)(implicit factory: Factory[B, That]): That = {
val seqOps = seq(coll)
factory.fromSpecific(new AbstractView[B] {
def iterator = new AbstractIterator[B] {
val it = seqOps.iterator
var intersperseNext = false
def hasNext = intersperseNext || it.hasNext
def next() = {
val elem = if (intersperseNext) sep else it.next()
intersperseNext = !intersperseNext && it.hasNext
elem
}
}
})
}
}
implicit def IntersperseOperation[Repr](coll: Repr)(implicit seq: IsSeq[Repr]): IntersperseOperation[Repr] =
new IntersperseOperation(coll, seq)
import scala.collection.{ AbstractIterator, AbstractView, Factory }
import scala.collection.generic.IsSeq
extension [Repr](coll: Repr)(using seq: IsSeq[Repr])
def intersperse[B >: seq.A, That](sep: B)(using factory: Factory[B, That]): That =
val seqOps = seq(coll)
factory.fromSpecific(new AbstractView[B]:
def iterator = new AbstractIterator[B]:
val it = seqOps.iterator
var intersperseNext = false
def hasNext = intersperseNext || it.hasNext
def next() =
val elem = if intersperseNext then sep else it.next()
intersperseNext = !intersperseNext && it.hasNext
elem
)
然而,如果我們嘗試,我們會得到以下行為
scala> List(1, 2, 3).intersperse(0)
res0: Array[Int] = Array(1, 0, 2, 0, 3)
我們會取得一個 Array
,儘管來源集合是一個 List
!的確,沒有任何東西會限制 intersperse
的結果類型取決於接收器類型。
若要產生一個類型取決於來源集合的集合,我們必須使用 scala.collection.BuildFrom
(以前稱為 CanBuildFrom
),而不是 Factory
。 BuildFrom
定義如下
trait BuildFrom[-From, -A, +C] {
/** @return a collection of type `C` containing the same elements
* (of type `A`) as the source collection `it`.
*/
def fromSpecific(from: From)(it: IterableOnce[A]): C
/** @return a Builder for the collection type `C`, containing
* elements of type `A`.
*/
def newBuilder(from: From): Builder[A, C]
}
trait BuildFrom[-From, -A, +C]:
/** @return a collection of type `C` containing the same elements
* (of type `A`) as the source collection `it`.
*/
def fromSpecific(from: From)(it: IterableOnce[A]): C
/** @return a Builder for the collection type `C`, containing
* elements of type `A`.
*/
def newBuilder(from: From): Builder[A, C]
BuildFrom
具有與 Factory
類似的操作,但它們會採用一個額外的 from
參數。在說明如何解析 BuildFrom
的隱含實例之前,讓我們先看看如何使用它。以下是根據 BuildFrom
實作的 intersperse
import scala.collection.{ AbstractView, BuildFrom }
import scala.collection.generic.IsSeq
class IntersperseOperation[Repr, S <: IsSeq[Repr]](coll: Repr, seq: S) {
def intersperse[B >: seq.A, That](sep: B)(implicit bf: BuildFrom[Repr, B, That]): That = {
val seqOps = seq(coll)
bf.fromSpecific(coll)(new AbstractView[B] {
// same as before
})
}
}
implicit def IntersperseOperation[Repr](coll: Repr)(implicit seq: IsSeq[Repr]): IntersperseOperation[Repr, seq.type] =
new IntersperseOperation(coll, seq)
import scala.collection.{ AbstractIterator, AbstractView, BuildFrom }
import scala.collection.generic.IsSeq
extension [Repr](coll: Repr)(using seq: IsSeq[Repr])
def intersperse[B >: seq.A, That](sep: B)(using bf: BuildFrom[Repr, B, That]): That =
val seqOps = seq(coll)
bf.fromSpecific(coll)(new AbstractView[B]:
// same as before
)
請注意,我們在 IntersperseOperation
類別中追蹤接收者集合 Repr
的類型。現在,考慮當我們撰寫下列表達式時會發生什麼事
List(1, 2, 3).intersperse(0)
編譯器必須解析類型為 BuildFrom[Repr, B, That]
的隱含參數。類型 Repr
受接收者類型 (在此為 List[Int]
) 約束,而類型 B
則由傳遞為分隔符號的值推論 (在此為 Int
)。最後,要產生的集合類型 That
由 BuildFrom
參數的解析結果固定。在我們的案例中,有一個 BuildFrom[List[Int], Int, List[Int]]
執行個體,將結果類型固定為 List[Int]
。
摘要
- 若要使用任何集合,請將
IterableOnce
(或更具體的項目,例如Iterable
、Seq
等) 作為參數,- 若還要支援
String
、Array
和View
,請使用IsIterable
,
- 若還要支援
- 若要產生具有其類型的集合,請使用
Factory
, - 若要根據來源集合的類型和要產生的集合元素的類型產生集合,請使用
BuildFrom
。