Как я могу гарантировать, что динамический тип моей пользовательской коллекции Scala будет сохранен во время карты ()? - PullRequest
4 голосов
/ 14 апреля 2011

Я прочитал очень интересную статью об архитектуре коллекций Scala 2.8 , и я немного экспериментировал с ней.Для начала я просто скопировал окончательный код для хорошего примера 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)

, но этот код преобразует РНК в вектор!

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

Это проблема, поскольку клиентский код не знает о RNAкласс может преобразовать его обратно в Vector вместо этого, когда отображается только от Base до Base.Почему это так, и как это исправить?

P.-S .: Я нашел предварительный ответ (см. Ниже), пожалуйста, поправьте меня, если я ошибаюсь.

Ответы [ 2 ]

3 голосов
/ 14 апреля 2011

Если статический тип переменной rna равен IndexedSeq[Base], автоматически вставленный CanBuildFrom не может быть тем, который определен в сопутствующем объекте RNA, поскольку компилятор не должен знать, что rna экземпляр RNA.

Так откуда это? Компилятор возвращается к экземпляру GenericCanBuildFrom, определенному в объекте IndexedSeq. GenericCanBuildFrom s создают свои компоновщики, вызывая genericBuilder[B] в исходной коллекции, и требование к этому универсальному компоновщику заключается в том, что он может создавать универсальные коллекции, которые могут содержать любой тип B - как, конечно, возвращаемый тип функции переданный в map() не ограничен.

В этом случае RNA является только IndexedSeq[Base], а не универсальным IndexedSeq, поэтому невозможно переопределить genericBuilder[B] в RNA, чтобы вернуть RNA -специфический компоновщик - мы бы получили чтобы проверить во время выполнения, является ли B Base или чем-то еще, но мы не можем этого сделать.

Я думаю, это объясняет , почему , в вопросе мы получаем Vector назад. Что касается того, как мы можем это исправить, это открытый вопрос ...

Редактировать : для исправления требуется map(), чтобы узнать, соответствует ли он подтипу A или нет. Для этого потребуется значительное изменение в библиотеке коллекций. См. Связанный вопрос Должна ли карта Scala () вести себя по-разному при сопоставлении с одним и тем же типом? .

1 голос
/ 14 апреля 2011

Почему я думаю, что статически печатать на более слабый тип, чем РНК, не очень хорошая идея.Это действительно должен быть комментарий (потому что это скорее мнение, но это будет сложнее читать).От вашего комментария до моего комментария:

Почему бы и нет?Как подкласс IndexedSeq [Base], RNA может делать все, что IndexedSeq [Base], согласно принципу подстановки Лискова.Иногда все, что вы знаете, это то, что это IndexedSeq, и вы все еще ожидаете, что фильтр, карта и друзья сохранят ту же конкретную реализацию.Фактически, фильтр делает это - но не сопоставление

filter делает это, потому что компилятор может статически гарантировать это.Если вы сохраняете элементы из определенной коллекции, вы получаете коллекцию того же типа.map не может гарантировать, что это зависит от передаваемой функции.

Моя точка зрения больше касается действия , явно указывающего тип и ожидающего большего, чем то, что он может доставить .Как пользователь коллекции RNA, я могу написать код, который зависит от определенных свойств этой коллекции, таких как эффективное представление памяти.

Итак, давайте предположим, что в val rna: IndexedSeq[Base] я утверждаю, что rna есть просто и IndexedSeq.Несколькими строками позже я вызываю метод doSomething(rna), где я ожидаю эффективного представления памяти, что будет лучшей подписью для этого?def doSomething[T](rna: IndexedSeq[Base]): T или def 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)

...