Наследование Скалы; Проблема строителя; Неуниверсальный IterableLike

Я пытаюсь реализовать простую цель проектирования, но сложность системы типов Scala вызывает у меня некоторую головную боль. После сравнения Traversable, Iterator, Iterable, Stream, View и т. Д. Я решил определить пользовательскую черту (давайте просто назовем ее Stream для краткости) что

  • не является универсальным (мой поток семантически имеет смысл только как некоторые Stream[StreamEntry] и я хочу избежать бессмысленных типов, таких как Stream[Int])
  • имеет аналогичное использование Iterable
  • все участники любят take, dropи т. д. должен вернуться Stream а не основной Iterable,

Это то, что я пробовал до сих пор:

Подход 1

Чтобы сделать набросок сценария использования, простым примером (который нарушает третью цель разработки) будет:

case class StreamEntry(data: Double) // just a dummy

trait Stream extends Iterable[StreamEntry] {
  val metaInfo: String
}

// example use case
val s = new Stream {
  val metaInfo = "something"
  val iterator = StreamEntry(1) :: StreamEntry(2) :: StreamEntry(3) :: Nil toIterator
}

val t = s.take(1) // unfortunately, this is no longer a Stream

Подход 2

Это третье требование требует использования шаблонной черты вместо базовой (я надеюсь, что это стандартная терминология для обозначения SomeCollection или SomeCollectionLike). Это означает, что я должен использовать IterableLike[StreamEntry, Stream] который переопределяет типы возврата представляющей коллекции так же, как Iterable продолжается IterableLike[A, Iterable[A]] возвращать Iterables. Моя идея заключалась в том, чтобы сделать почти так же, как Iterable делает. Это было бы:

// this is exactly the way `Iterable` is defined, but non-generic
trait Stream extends Traversable[StreamEntry]
             with GenIterable[StreamEntry]
             with GenericTraversableTemplate[StreamEntry, Stream]
             with IterableLike[StreamEntry, Stream] {
  ...
}

К сожалению, это не компилируется, потому что Stream появляется в качестве аргумента шаблона для GenericTraversableTemplate и компилятор теперь требует аргумент шаблона (ровно один) для Stream сам, что имеет смысл.

Подход 3, 4, ...

Отсюда я заблудился в системе типов. Просто удаляя with GenericTraversableTemplate приводит к несовместимому типу newBuilder и незаконное наследование из-за конфликтов в параметрах типа в GenericTraversableTemplate от GenInterable а также Traversable,

Возможно, самым близким решением было следующее:

trait Stream extends TraversableLike[StreamEntry, Stream] 
             with IterableLike[StreamEntry, Stream] {
  val metaInfo: String
  def seq = this
  def newBuilder: scala.collection.mutable.Builder[StreamEntry, Stream] = ???
}

Это компилируется, но, к сожалению, я понятия не имею, как реализовать Builder. Можно ли повторно использовать универсальный Builder для моей неуниверсальной черты? На самом деле я, хотя я могу обойтись без Строителя, потому что я никогда не хочу строить новый Stream из других коллекций. Но в настоящее время я испытываю некоторое странное поведение во время выполнения с этим подходом, который я не могу полностью понять. Например:

val s = new Stream {
  val metaInfo = "something"
  val iterator = StreamEntry(1) :: StreamEntry(2) :: StreamEntry(3) :: Nil toIterator
}

// executing the following independently (not in sequence) results in:

s.take(1)    // throws: scala.NotImplementedError: an implementation is missing
             // seems to require a Builder :(
s.toArray    // works
s.toIterator // works
s.toIterable // throws: java.lang.ClassCastException: cannot be cast to scala.collection.Iterable

Теперь я чувствую себя немного потерянным в глубине системы типов Scala. Я все еще на правильном пути с этим последним подходом, и Строитель - просто недостающая часть в этой загадке?

И как будет выглядеть реализация Builder для неуниверсального некэшируемого типа? Простая идея для реализации += было бы использовать некоторый изменяемый буфер, но это было бы очень против использования итераторов в первую очередь... И как я должен реализовать to член, если я не знаю, как построить класс этого типа? Я думаю, что весь соответствующий код должен быть где-то в библиотеке, я просто не могу выкопать его.

1 ответ

Вот Это Да! У тебя там много чего происходит...

Вот некоторые вещи, которые вы должны знать или учитывать при решении этой проблемы проектирования...

Терминология:

  • Мы не ссылаемся на "шаблоны", мы называем их "универсальными" или "параметризованными" типами. Причина этого в том, что эти типы не являются шаблонами! То есть они не заполнены фактическими параметрами типа для создания новых классов каждый раз, когда они используются (как в случае с C++, который правильно использует термин "шаблон"). Вместо этого создается только один класс (*), который обслуживает каждый экземпляр этого универсального типа с определенными параметрами типа.

Факторы дизайна и языка:

Ты говоришь:

... не является универсальным (мой поток семантически имеет смысл только как какой-то Stream[StreamEntry], и я хочу избежать бессмысленных типов, таких как Stream[Int])

Требование не распространяется на неуниверсальный класс. Скорее, это суть того, что такое "привязка типа". Например:

class Generic[T <: UpperBound](ctorArgs...) {
}

В этом случае класс Generic могут быть созданы только с типами, которые являются подтипами UpperBound, (Обратите внимание, что всякий раз, когда мы говорим "подтип", мы подразумеваем рефлексивное отношение подтипа. Другими словами, каждый тип является подтипом самого себя согласно этому определению.

Итог:

Интересно, что ваш "потоковый" класс является или делает или имеет, что не удовлетворяет существующий тип в стандартной библиотеке Scala? Как вы обнаружили, расширение стандартных библиотечных классов не совсем тривиально, хотя, безусловно, выполнимо. Я думаю, что это не элементарное упражнение в программировании Scala, и, вероятно, его не следует пытаться сделать одним из первых набегов в Scala.

(*) Это упрощение, которое достаточно для целей этого объяснения.

Другие вопросы по тегам