编写接受 IndexedSeq[A] 和 ParVector[A] 的多态函数?

write polymorphic function that accept IndexedSeq[A] as well as ParVector[A]?

我想编写一个接受 IndexedSeq[A] 或 ParVector[A] 的多态函数。在函数内部,我想访问前置方法,即 SeqLike 中的 +:。 SeqLike like 对我来说是一个相当混乱的类型,因为它需要一个 Repr ,我有点忽略了,当然没有成功。

def goFoo[M[_] <: SeqLike[_,_], A](ac: M[A])(p: Int): M[A] = ???

该函数应该接受一个空累加器作为开始并递归调用自身 p 次并且每次都在前面添加一个 A。这是一个具体的例子

def goStripper[M[_] <: SeqLike[_,_]](ac: M[PDFTextStripper])(p: Int): M[PDFTextStripper] = {
  val str = new PDFTextStripper
  str.setStartPage(p)
  str.setEndPage(p)
  if (p > 1) goStripper(str +: ac)(p-1)
  else str +: ac
}

但是当然这不会编译,因为我缺少一些关于 SeqLike 的基本知识。有没有人有解决方案(最好对此有解释?)

谢谢。

处理 SeqLike[A, Repr] 有时会有点困难。您确实需要很好地了解集合库的工作原理(如果您有兴趣,这是一篇很棒的文章,http://docs.scala-lang.org/overviews/core/architecture-of-scala-collections.html)。值得庆幸的是,在您的情况下,您实际上甚至不需要担心太多IndexedSeq[A]ParVector[A] 都是 scala.collection.GenSeq[A] 的子类。所以你可以只写你的方法如下

简单的解决方案

scala> def goFoo[A, B <: GenSeq[A] with GenSeqLike[A, B]](ac: B)(p: Int): B = ac
goFoo: [A, B <: scala.collection.GenSeq[A] with scala.collection.GenSeqLike[A,B]](ac: B)(p: Int)B

scala> goFoo[Int, IndexedSeq[Int]](IndexedSeq(1))(1)
res26: IndexedSeq[Int] = Vector(1)

scala> goFoo[Int, ParVector[Int]](new ParVector(Vector(1)))(1)
res27: scala.collection.parallel.immutable.ParVector[Int] = ParVector(1)

您需要强制 BGenSeq[A]GenSeqLike[A, Repr] 的子类型,以便您可以为 Repr 提供正确的值。您还需要强制 GenSeqLike[A, Repr] 中的 ReprB。否则某些方法不会 return 正确的类型。 Repr 是集合的底层表示。要真正理解它,您应该阅读我链接的文章,但您可以将其视为许多集合操作的输出类型,尽管这过于简单化了。如果您真的感兴趣,我会在下面详细讨论。现在,只要说我们希望它与我们正在操作的集合是同一类型就足够了。

更高级的解决方案

现在,类型系统需要您手动提供两个泛型参数,这很好,但我们可以做得更好。如果你允许更高的种类,你可以让它更干净一些。

scala> import scala.language.higherKinds
import scala.language.higherKinds

scala> def goFoo[A, B[A] <: GenSeq[A] with GenSeqLike[A, B[A]]](ac: B[A])(p: Int): B[A] = ac
goFoo: [A, B[A] <: scala.collection.GenSeq[A] with scala.collection.GenSeqLike[A,B[A]]](ac: B[A])(p: Int)B[A]

scala> goFoo(IndexedSeq(1))(1)
res28: IndexedSeq[Int] = Vector(1)

scala> goFoo(new ParVector(Vector(1)))(1)
res29: scala.collection.parallel.immutable.ParVector[Int] = ParVector(1)

现在您不必担心手动提供类型。

递归

这些解决方案也适用于递归。

scala> @tailrec
     | def goFoo[A, B <: GenSeq[A] with GenSeqLike[A, B]](ac: B)(p: Int): B = 
     | if(p == 0){
     |   ac 
     | } else {
     |   goFoo[A, B](ac.drop(1))(p-1)
     | }
goFoo: [A, B <: scala.collection.GenSeq[A] with scala.collection.GenSeqLike[A,B]](ac: B)(p: Int)B

scala> goFoo[Int, IndexedSeq[Int]](IndexedSeq(1, 2))(1)
res30: IndexedSeq[Int] = Vector(2)

以及更高版本

scala> @tailrec
     | def goFoo[A, B[A] <: GenSeq[A] with GenSeqLike[A, B[A]]](ac: B[A])(p: Int): B[A] = 
     | if(p == 0){
     |   ac 
     | } else {
     |   goFoo(ac.drop(1))(p-1)
     | }
goFoo: [A, B[A] <: scala.collection.GenSeq[A] with scala.collection.GenSeqLike[A,B[A]]](ac: B[A])(p: Int)B[A]

scala> goFoo(IndexedSeq(1, 2))(1)
res31: IndexedSeq[Int] = Vector(2)

直接使用GenSeqLike[A, Repr] TL;DR

所以我只想说,除非您需要更通用的解决方案,否则不要这样做。它是最难理解和使用的。我们不能使用 SeqLike[A, Repr],因为 ParVector 不是 SeqLike 的实例,但我们可以使用 GenSeqLike[A, Repr]ParVector[A]IndexedSeq[A]子类。

话虽如此,让我们来谈谈如何直接使用 GenSeqLike[A, Repr] 来解决这个问题。

解压类型变量

首先是简单的

一个

这只是集合中值的类型,因此对于 Seq[Int] 这将是 Int

代表

这是集合的基础类型

Scala 集合在共同特征中实现了大部分功能,因此它们不必到处重复代码。此外,他们希望允许 out of band 类型像集合一样运行,即使它们不是从集合特征继承的(我在看你 Array ),并允许客户 libraries/programs 非常轻松地添加自己的集合实例,同时获得大量免费定义的集合方法。

它们的设计有两个指导约束

注意:这些示例摘自上述文章,并非我自己的。(为了完整起见,此处再次链接http://docs.scala-lang.org/overviews/core/architecture-of-scala-collections.html

第一个约束可以在下面的例子中展示。 BitSet 是一组非负整数。如果我执行以下操作,结果应该是什么?

BitSet(1).map(_+1): ???

正确答案是 BitSet。我知道这看起来很明显,但请考虑以下内容。此操作的类型是什么?

BitSet(1).map(_.toFloat): ???

不可能BitSet,对吧?因为我们说过 BitSet 值是非负整数。所以结果是SortedSet[Float].

Repr 参数结合适当的 CanBuildFrom 实例(我稍后解释这是什么)是允许 returning 的主要机制之一最具体的类型可能。我们可以通过在 REPL 上欺骗系统来看到这一点。考虑以下内容,VectorIndexedSeqSeq 的子类。那么如果我们这样做呢...

scala> val x: GenSeqLike[Int, IndexedSeq[Int]] = Vector(1)
x: scala.collection.SeqLike[Int,IndexedSeq[Int]] = Vector(1)

scala> 1 +: x
res26: IndexedSeq[Int] = Vector(1, 1)

看看这里的最终类型是怎样的IndexedSeq[Int]。这是因为我们告诉类型系统集合的底层表示是 IndexedSeq[Int] 所以它会尽可能地尝试 return 该类型。现在看这个,

scala> val x: GenSeqLike[Int, Seq[Int]] = Vector(1)
x: scala.collection.SeqLike[Int,Seq[Int]] = Vector(1)

scala> 1 +: x
res27: Seq[Int] = Vector(1, 1)

现在我们得到一个 Seq 出来。

因此 Scala 集合尝试为您的操作提供最具体的类型,同时仍然允许大量代码重用。他们通过利用 Repr 类型来做到这一点,因为我们作为 CanBuildFrom(仍在使用它)我知道你可能想知道这与你的问题有什么关系,别担心我们正在现在。我不会对 Liskov 的替换原则说什么,因为它与你的具体问题关系不大(但你仍然应该阅读它!)

好的,现在我们明白 GenSeqLike[A, Repr] 是 scala 集合用来重用 Seq(以及 Seq 等其他东西)代码的特征。我们了解到 Repr 用于存储底层集合表示,以帮助将集合类型告知 return。我们还没有解释最后一点是如何工作的,所以让我们现在就开始吧!

CanBuildFrom[-From, -Elem, +To]

一个CanBuildFrom实例是集合库如何知道如何构建给定操作的结果类型。例如 SeqLike[A, Repr]+: 方法的 real 类型是这样的。

 abstract def +:[B >: A, That](elem: B)(implicit bf: CanBuildFrom[Repr, B, That]): That 

这意味着为了将元素添加到 GenSeqLike[A, Repr] 我们需要一个 CanBuildFrom[Repr, B, That] 的实例,其中 Repr 是我们当前集合的类型,B是我们集合中元素的超类型,That 是操作完成后我们将拥有的集合类型。我不打算深入了解 CanBuildFrom 的工作原理(详细信息请再次参阅链接文章),现在请相信我,这就是它的作用。

综合起来

所以现在我们准备构建一个 goFoo 的实例,它适用于 GenSeqLike[A, Repr] 个值。

scala> def goFoo[A, Repr <: GenSeqLike[A, Repr]](ac: Repr)(p: Int)(implicit cbf: CanBuildFrom[Repr, A, Repr]): Repr = ac
goFoo: [A, Repr <: scala.collection.GenSeqLike[A,Repr]](ac: Repr)(p: Int)(implicit cbf: scala.collection.generic.CanBuildFrom[Repr,A,Repr])Repr

scala> goFoo[Int, IndexedSeq[Int]](IndexedSeq(1))(1)
res7: IndexedSeq[Int] = Vector(1)

scala> goFoo[Int, ParVector[Int]](new ParVector(Vector(1)))(1)
res8: scala.collection.parallel.immutable.ParVector[Int] = ParVector(1)

我们在这里说的是,有一个 CanBuildFrom 将在元素 A 上采用类型 ReprGenSeqLike 的子类并构建一个新的Repr。这意味着我们可以对 Repr 类型执行任何操作,这将导致一个新的 Repr,或者在特定情况下一个新的 ParVectorIndexedSeq.

不幸的是,我们必须手动提供泛型参数,否则类型系统会变得混乱。谢天谢地,我们可以再次使用更高的种类来避免这种情况,

scala> def goFoo[A, Repr[A] <: GenSeqLike[A, Repr[A]]](ac: Repr[A])(p: Int)(implicit cbf: CanBuildFrom[Repr[A], A, Repr[A]]): Repr[A] = ac
goFoo: [A, Repr[A] <: scala.collection.GenSeqLike[A,Repr[A]]](ac: Repr[A])(p: Int)(implicit cbf: scala.collection.generic.CanBuildFrom[Repr[A],A,Repr[A]])Repr[A]

scala> goFoo(IndexedSeq(1))(1)
res16: IndexedSeq[Int] = Vector(1)

scala> goFoo(new ParVector(Vector(1)))(1)
res17: scala.collection.parallel.immutable.ParVector[Int] = ParVector(1)

所以这很好,因为它比使用 GenSeq 更通用一点,但它也 更令人困惑。除了思想实验,我不建议这样做。

结论

虽然希望直接使用 GenSeqLike 了解 scala 集合的工作原理,但我很难想到我会真正推荐它的用例。该代码难以理解,难以使用,并且很可能有一些我错过的边缘情况。一般来说,我建议尽可能避免与 scala 集合实现特征交互,例如 GenSeqLike ,除非您将自己的集合安装到系统中。您仍然需要轻触 GenSeqLike 以获得 GenSeq 中的所有操作,方法是给它正确的 Repr 类型,但您可以避免考虑 CanBuildFrom 值。