新增自訂集合操作(Scala 2.13)

語言

Julien Richard-Foy

本指南說明如何撰寫可套用至任何集合類型並傳回相同集合類型的操作,以及如何撰寫可透過要建置的集合類型參數化的操作。建議先閱讀有關 集合架構 的文章。

下列各節說明如何使用產生轉換任何集合類型。

使用任何集合

在第一節中,我們展示如何撰寫使用任何集合實例的方法,該實例是集合階層的一部分。我們在第二節中展示如何支援類集合類型,例如 StringArray (它們不延伸 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 集合階層。不過,它們可以轉換為適當的集合類型,但延伸方法無法直接在 StringArray 上執行,因為這需要連續套用兩個隱式轉換。

我們可以將 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 能夠在任何類似集合的類型上執行,例如 StringArray,我們必須再增加一層間接層級

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)。實際集合類型和 StringArray 都有實例。

我們希望 sumBy 能夠在任何類似集合的類型上執行,例如 StringArray。幸運的是,類型 IsIterable[Repr] 具有所有可以轉換為 IterableOps[A, Iterable, C] 的類型 Repr 的隱式實例(對於某些元素類型 A 和某些集合類型 C),而且實際集合類型和 StringArray 都有實例。

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 作為參數,而不是 IterableIterableOnce,但這僅適用於實際 Seq 值。如果您想支援 StringArray 值,您必須使用 IsSeqIsSeq 類似於 IsIterable,但提供轉換為 SeqOps[A, Iterable, C](對於某些類型 AC)。

使用 IsSeq 也需要讓您的運算式對 SeqView 值起作用,因為 SeqView 沒有延伸 Seq。類似地,有一個 IsMap 類型,讓運算式同時對 MapMapView 值起作用。

在這種情況下,同樣地,最直接的解決方案是將 Seq 作為參數,而不是 IterableIterableOnce。類似於 IsIterableIsSeq 提供轉換為 SeqOps[A, Iterable, C](對於某些類型 AC)。

IsSeq 也讓你的操作在 SeqView 值上運作,因為 SeqView 沒有延伸 Seq。類似地,有一個 IsMap 類型,讓操作同時在 MapMapView 值上運作。

產生任何集合

這種情況發生在函式庫提供一個操作來產生集合,同時讓使用者選擇精確的集合類型。

例如,考慮一個類型類別 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)。然後它呼叫 FactoryfromSpecific 方法,以建立使用者預期的集合。

轉換任何集合

轉換集合包含使用和產生集合。這是透過結合先前各節中所述的技巧來達成。

例如,我們想實作 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),而不是 FactoryBuildFrom 定義如下

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)。最後,要產生的集合類型 ThatBuildFrom 參數的解析結果固定。在我們的案例中,有一個 BuildFrom[List[Int], Int, List[Int]] 執行個體,將結果類型固定為 List[Int]

摘要

  • 若要使用任何集合,請將 IterableOnce (或更具體的項目,例如 IterableSeq 等) 作為參數,
    • 若還要支援 StringArrayView,請使用 IsIterable
  • 若要產生具有其類型的集合,請使用 Factory
  • 若要根據來源集合的類型和要產生的集合元素的類型產生集合,請使用 BuildFrom

此頁面的貢獻者