添加自定义集合操作(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] 特征提供了两种从类型为 A 的元素构建集合 C 的方法

  • 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 的隐式实例之前,让我们先看看如何使用它。以下是基于 BuildFromintersperse 的实现

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

此页面的贡献者