如何确保在map()期间保留自定义Scala集合的动态类型?

时间:2011-04-14 08:46:20

标签: scala type-inference scala-collections static-typing

我读了非常有趣的article on the architecture of the Scala 2.8 collections,我一直在试验它。首先,我只是复制了很好的RNA示例的最终代码。这里仅供参考:

abstract class Base
case object A extends Base
case object T extends Base
case object G extends Base
case object U extends Base

object Base {
  val fromInt: Int => Base = Array(A, T, G, U)
  val toInt: Base => Int = Map(A -> 0, T -> 1, G -> 2, U -> 3)
}

final class RNA private (val groups: Array[Int], val length: Int)
    extends IndexedSeq[Base] with IndexedSeqLike[Base, RNA] {

  import RNA._

  // Mandatory re-implementation of `newBuilder` in `IndexedSeq`
  override protected[this] def newBuilder: Builder[Base, RNA] =
    RNA.newBuilder

  // Mandatory implementation of `apply` in `IndexedSeq`
  def apply(idx: Int): Base = {
    if (idx < 0 || length <= idx)
      throw new IndexOutOfBoundsException
    Base.fromInt(groups(idx / N) >> (idx % N * S) & M)
  }

  // Optional re-implementation of foreach, 
  // to make it more efficient.
  override def foreach[U](f: Base => U): Unit = {
    var i = 0
    var b = 0
    while (i < length) {
      b = if (i % N == 0) groups(i / N) else b >>> S
      f(Base.fromInt(b & M))
      i += 1
    }
  }
}

object RNA {

  private val S = 2 // number of bits in group
  private val M = (1 << S) - 1 // bitmask to isolate a group
  private val N = 32 / S // number of groups in an Int

  def fromSeq(buf: Seq[Base]): RNA = {
    val groups = new Array[Int]((buf.length + N - 1) / N)
    for (i <- 0 until buf.length)
      groups(i / N) |= Base.toInt(buf(i)) << (i % N * S)
    new RNA(groups, buf.length)
  }

  def apply(bases: Base*) = fromSeq(bases)

  def newBuilder: Builder[Base, RNA] =
    new ArrayBuffer mapResult fromSeq

  implicit def canBuildFrom: CanBuildFrom[RNA, Base, RNA] =
    new CanBuildFrom[RNA, Base, RNA] {
      def apply(): Builder[Base, RNA] = newBuilder
      def apply(from: RNA): Builder[Base, RNA] = newBuilder
    }
}

现在,这是我的问题。如果我这样做,一切都很好:

val rna = RNA(A, G, T, U)
println(rna.map(e => e)) // prints RNA(A, G, T, U)

但是这段代码将RNA转换为Vector!

val rna: IndexedSeq[Base] = RNA(A, G, T, U)
println(rna.map(e => e)) // prints Vector(A, G, T, U)

这是一个问题,因为不知道RNA类的客户端代码可能会在仅VectorBase的映射时将其转换回Base。为什么会这样,有什么方法可以解决它?

P.-S。:我找到了一个试探性的答案(见下文),如果我错了,请纠正我。

2 个答案:

答案 0 :(得分:3)

如果rna变量的静态类型为IndexedSeq[Base],则自动插入的CanBuildFrom不能是RNA伴随对象中定义的那个,因为编译器不是应该知道rnaRNA的实例。

那么它来自哪里?编译器依赖于GenericCanBuildFrom的实例,IndexedSeq对象中定义的实例。 GenericCanBuildFrom通过在原始集合上调用genericBuilder[B]来生成构建器,并且该泛型构建器的要求是它可以生成可以包含任何类型B的泛型集合 - 当然,传递给map()的函数的返回类型不受约束。

在这种情况下,RNA只是IndexedSeq[Base]而非通用IndexedSeq,因此无法覆盖genericBuilder[B]中的RNA返回一个RNA特定的构建器 - 我们必须在运行时检查BBase还是其他东西,但我们不能这样做。

我认为这解释了为什么,在这个问题中,我们得到了Vector。至于我们如何解决它,这是一个悬而未决的问题......

修改:修复此问题需要map()知道它是否映射到A的子类型。为此,需要对集合库进行重大更改。请参阅相关问题Should Scala's map() behave differently when mapping to the same type?

答案 1 :(得分:1)

为什么我认为静态输入比RNA更弱的类型并不是一个好主意。它应该是一个评论(因为它更多的是一个意见,但这将更难阅读)。从您的评论到我的评论:

  

为什么不呢?作为IndexedSeq [Base]的子类,根据Liskov替换原则,RNA能够完成IndexedSeq [Base]所做的一切。有时,你所知道的只是它是一个IndexedSeq,你仍然期望过滤器,地图和朋友保持相同的具体实现。实际上,过滤器会这样做 - 但不会映射

filter这样做是因为编译器可以静态地保证它。如果保留特定集合中的元素,则最终会得到相同类型的集合。 map不能保证,它取决于传递的函数。

我的观点更多地是关于明确指定一个类型并期望能够提供更多内容的行为。作为RNA集合的用户,我可能会编写依赖于此集合的某些属性的代码,例如高效的内存表示。

因此,我假设我在val rna: IndexedSeq[Base]中说明rna 只是 IndexedSeq。几行之后我调用了一个方法doSomething(rna),我期望有效的内存表示,那个最好的签名是什么? def doSomething[T](rna: IndexedSeq[Base]): Tdef doSomething[T](rna: RNA): T

我认为应该是后者。但如果是这种情况,那么代码将无法编译,因为rna不是静态的RNA对象。如果方法签名应该是前者,那么实质上我说我不关心内存表示效率。因此,我认为明确指定较弱类型但期望更强行为的行为是矛盾的。这是你在你的例子中做的。

现在我确实看到了,即使我这样做了:

val rna = RNA(A, G, T, U)
val rna2 = doSomething(rna)

其他人写道:

def doSomething[U](seq: IndexedSeq[U]) = seq.map(identity)

我希望rna2成为RNA对象,但这不会发生......这意味着其他人应该编写一个方法,如果他们需要CanBuildFrom希望让呼叫者获得更具体的类型:

def doSomething[U, To](seq: IndexedSeq[U])
   (implicit cbf: CanBuildFrom[IndexedSeq[U], U, To]) = seq.map(identity)(cbf)

然后我可以致电:val rna2: RNA = doSomething(rna)(collection.breakOut)