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]
特征提供了两种从类型为 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
)。然后它调用 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
。