Нестрогие, неизменяемые, незапоминаемые бесконечные серии в Scala

Мне нужен бесконечный нестрогий ряд x1, x2, x3... чтобы я мог работать с одним элементом за раз , не запоминая результаты, чтобы поддерживать постоянное использование памяти. Ради конкретики скажем, что это ряд целых чисел (например, натуральные числа, нечетные числа, простые числа), хотя этот вопрос может относиться к более общим типам данных.

Самый простой способ работать с бесконечными списками — использовать объект Stream в Scala. Распространенной идиомой является написание функции, которая возвращает Stream, использует оператор #:: для добавления термина в серию, а затем рекурсивно вызывает саму себя. Например, следующее генерирует бесконечный поток целых чисел с заданным начальным значением и последующей функцией.

  def infiniteList(n: Int, f: Int => Int): Stream[Int] = {
      n #:: infiniteList(f(n), f)
  }
  infiniteList(2, _*2+3).take(10) print
  // returns 2, 7, 17, 37, 77, 157, 317, 637, 1277, 2557, empty

(Я понимаю, что приведенное выше эквивалентно библиотечному вызову Stream.iterate(2)(_*2+3). Я написал его здесь как пример этой бесконечной идиомы Stream.)

Однако потоки запоминают свои результаты, что делает их требования к памяти непостоянными и потенциально очень большими. В документации говорится, что мемоизация избежать, если вы не держитесь за голову Stream, но на практике это может быть сложно. Я могу реализовать код с бесконечным списком, в котором, как мне кажется, думаю, я не удерживаю какие-либо заголовки потока, но если у него все еще есть неограниченные требования к памяти, я должен выяснить, не заключается ли проблема в том, что я m обрабатывая мои потоки каким-то образом, который вызывает мемоизацию или что-то еще. Это может быть трудной задачей отладки и иметь запах кода, потому что я пытаюсь обмануть явно запомненную структуру данных, чтобы она возвращала не запомненный результат.

Я хочу что-то с семантикой Stream ожидать без мемоизации. Не похоже, чтобы такой объект существовал в Scala. Я экспериментировал с использованием итераторов для реализации бесконечных числовых рядов, но изменчивость итераторов делает это сложным, когда вы начинаете хотеть выполнять над ними операции понимания. Я также пытался написать свой собственный код с нуля, но неясно, с чего мне начать (сделать ли мне подкласс Traversable?) или как избежать повторной реализации функциональности в map, fold и т. д.

Есть ли у кого-нибудь хороший пример кода Scala для реализации нестрогого, неизменного, незапоминаемого бесконечного списка?

В более общем плане я понимаю семантику traversable, iterable, sequence, поток и просмотр, но тот факт, что я нахожу этот вопрос таким неприятным, заставляет меня чувствовать, что я что-то неправильно понимаю. Мне кажется, что нестрогость и отсутствие запоминания являются полностью ортогональными свойствами, но Scala, похоже, приняла дизайнерское решение объединить их в Stream и не дала простого способа разделить их. Является ли это упущением со стороны Scala, или есть какая-то глубокая связь между нестрогостью и отсутствием запоминания, которую я упускаю из виду?


Я понимаю, что вопрос довольно абстрактный. Вот некоторый дополнительный контекст, который связывает его с конкретной проблемой.

Я сталкиваюсь с этой проблемой в ходе реализации генератора простых чисел, как описано в статье Мейссы О'Нил "Настоящее сито Эратосфена", и трудно привести простой пример того, где объект Iterator не подходит, не приведя множество деталей из этой статьи. Вот полная реализация с использованием потоков, очень элегантная, но потребляющая подозрительно много памяти.

Вот упрощенная реализация с итераторами, которая не компилируется, но дает представление о том, чего я хочу.

import scala.collection.mutable

object ONeillSieve {

  class NumericSeries extends BufferedIterator[Int] with Ordered[NumericSeries] {
    def hasNext = true

    def compare(that: NumericSeries) = that.head.compare(head)

    override def toString() = head + "..."

    var head = 3

    def next() = {
      val r = head
      head += 2
      r
   }
 }

def main(args: Array[String]) {
    val q = mutable.PriorityQueue[NumericSeries]()

    val odds = new NumericSeries

    q += odds.map(odds.head * _)
    odds.next()
    q += odds.map(odds.head * _)

    println("Sieve = %s\nInput = %s".format(q, odds))
  }
}

Мне нужно построить PriorityQueue бесконечных числовых рядов с наименьшим элементом. (Поэтому я использую BufferedIterator вместо простого Iterator.) Также обратите внимание, что здесь основой для бесконечных рядов являются нечетные целые числа, но наиболее общее решение включает более сложные ряды. В конце основной функции я хочу, чтобы очередь содержала две бесконечные серии:

  1. 3x(шансы, начиная с 3) (т.е. 9,12,15...)
  2. 5x(шансы начиная с 5) (т.е. 25,30,35...)

Вышеприведенное не компилируется, потому что odds.map(...) возвращает Iterator, а не NumericSeries и, следовательно, не может быть добавлен в очередь приоритетов.

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


person W.P. McNeill    schedule 12.01.2013    source источник
comment
Не могли бы вы показать пример, где Iterator падает на лицо? Я делаю это с Iterator, но вам может понадобиться пара трюков, чтобы заставить его работать во всех случаях, которые вы хотите. Так что, если мы увидим пример того, как наивный подход подводит вас, возможно, мы сможем помочь.   -  person Rex Kerr    schedule 13.01.2013
comment
В моем реальном приложении (реализация алгоритма решета Эратосфена Мелиссы О'Нил) я передаю эти серии и вызываю операции map и tail, которые в конечном итоге заставляют меня писать шаблонный код, такой как конструкторы копирования. У меня есть версия Stream с очень чистым исходным кодом, но ей не хватает памяти.   -  person W.P. McNeill    schedule 13.01.2013
comment
Можете ли вы показать свернутый пример того шаблона, которого вы хотите избежать? Кроме того, мемоизация некоторого вида является единственной причиной эффективности этого алгоритма.   -  person Rex Kerr    schedule 13.01.2013
comment
Я пытаюсь придумать упрощенный пример прямо сейчас. Это сложно. Чтобы реализовать алгоритм О'Нилла, вам нужен бесконечный список целых чисел, сгенерированных функцией-преемником, вы должны иметь возможность преобразовать этот ряд с map(_ * x) в несколько других рядов с разными коэффициентами масштабирования, которые все могут быть расширены независимо друг от друга.   -  person W.P. McNeill    schedule 13.01.2013


Ответы (4)


EDIT: Приведенное ниже не сохраняет тип Generator при использовании filter или map; действительно, попытка реализовать полный «MyType» для генератора более или менее невозможна (загляните в исходный код IndexedSeqView, чтобы увидеть беспорядок).

Но есть еще более простой способ (см. мой третий ответ)


Хорошо, моя вторая попытка. Чтобы сохранить ленивое поведение для map, filter и т. д., лучше всего создать подкласс SeqView или StreamView:

import collection.immutable.StreamView

final case class Generator[A](override val head: A, fun: A => A)
extends StreamView[A, Generator[A]] {
  protected def underlying = this
  def length: Int = Int.MaxValue  // ?
  def iterator = Iterator.iterate(head)(fun)
  def apply(idx: Int): A = {
    if(idx < 0) throw new IndexOutOfBoundsException(idx.toString)
    var res = head
    var i = idx; while(i > 0) {
      res = fun(res)
      i -= 1
    }
    res
  }
}

(Я принял предложение Рекса назвать это «Генератор»).

val i = Generator[Int](2, _ * 2 + 3)
i.take(4).foreach(println)
val j = i.map(_ * 0.5)
j.take(4).foreach(println)
person 0__    schedule 12.01.2013
comment
Вы хотите, чтобы Generator расширил SeqView или StreamView? (У вас есть последнее в фрагменте кода выше?) - person W.P. McNeill; 13.01.2013
comment
Я думаю, что у вас хорошее начало (+1), но вам, вероятно, придется добавить еще несколько черт, чтобы случайные методы не возвращали вам SeqView (или StreamView), когда вы хотите Generator. - person Rex Kerr; 13.01.2013
comment
StreamView является подклассом SeqView. Несмотря на название, он не запоминается. Это выглядело самым простым подходом. Единственное, что вышеприведенное еще не делает, это возвращает Generator при вызове карты, а StreamView. - person 0__; 13.01.2013
comment
Я попробовал ваши map, filter вернуть объект Generator для подхода, который вы здесь описываете, и неясно, какой должна быть семантика построителя, поскольку кажется, что строители хотят быть конечными изменяемыми объектами (ArrayBuffer или что-то еще), а моя серия бесконечна на строительство. - person W.P. McNeill; 13.01.2013

Если вам просто нужно несколько раз повторить список, попробуйте работать с Unit => Iterator[A] вместо оригинала, попробуйте эту реструктуризацию:

// Old way
val i = Iterator.tabulate(5)(_ + 2)
val j = i.map(_*5)
val k = i.map(_*3)
println(j.mkString(" "))  // Prints 10, 15, 20, 25, 30 as it should
println(k.mkString(" "))  // Prints nothing!  (i was used up!)

// New way
val f = (u: Unit) => Iterator.tabulate(5)(_+2)
val g = f andThen (_.map(_*5))
val h = f andThen (_.map(_*3))
println(g(()).mkString(" "))  // 10, 15, 20, 25, 30
println(h(()).mkString(" "))  // 6, 9, 12, 15, 18

Но все это снова работает с самого начала. Если вам нужно создать новую работу из середины, есть также способ сделать это, если вы готовы хранить все промежуточные элементы между тем, как далеко вы продвинулись:

val a = Iterator.tabulate(5)(_+2)
val (a1,a2) = a.duplicate
val c = a1.map(_*5)
val d = a2.map(_*3)
println(c.mkString(" "))  // 10, 15, 20, 25, 30...but stores a=2, 3, 4, 5, 6
println(d.mkString(" "))  // 6, 9, 12, 15, 18

Если ни тот, ни другой шаблон недостаточно хороши, вам придется создать класс в библиотеке коллекций — назовем его, может быть, Generator — который будет делать именно то, что вы хотите. Я бы наследовал от Iterator или Iterable, переопределяя или создавая метод duplicate, который создаст две новые копии с внутренней функцией генерации и данными в том же состоянии.

person Rex Kerr    schedule 12.01.2013
comment
Похоже, мне нужно использовать собственный подход, который наследуется от Iterator. Расширение классов коллекций — это один из аспектов Scala, который я до сих пор сбиваю с толку. Я никогда не знаю, с чего начать. - person W.P. McNeill; 13.01.2013
comment
@ W.P.McNeill - Я думаю, вам придется стиснуть зубы и начать (с руководством!). Я сомневаюсь, что вы обнаружите, что это работает так, как вы хотите, с учетом вашего приложения (но я оставлю ответ здесь независимо от тех, у кого могут быть менее требовательные задачи). - person Rex Kerr; 13.01.2013

Надеюсь, это самый прямой подход. Просто создайте ленивый Iterable:

object Generator {
  def apply[A](head: A)(next: A => A): Generator[A] = {
    val _head = head
    new collection.IterableView[A, Nothing] {
      override def head = _head
      def underlying = sys.error("No underlying structure")
      def iterator = Iterator.iterate(head)(next)
    }
  }
}
type Generator[A] = Iterable[A]

Вот вариант использования:

val q = collection.mutable.PriorityQueue[Generator[Int]]()
val odds = Generator(3)(_ + 2)
q += odds.map(odds.head * _)
val next = odds.tail
q += next.map(next.head * _)
q.last.take(3).mkString(",") // -> 9,12,21
q.head.take(3).mkString(",") // -> 25,35,45
person 0__    schedule 13.01.2013

EDIT: я оставляю этот ответ здесь для справки, но я обнаружил, что, чтобы не столкнуться с переполнением стека, лучше всего использовать коллекцию, которая по умолчанию ленива: SeqView -> см. мой другой ответ.


Если вы хотите определить новый тип коллекции, я бы себе это представил:

import collection.generic.{GenericTraversableTemplate, GenericCompanion}
import collection.immutable.LinearSeq

final case class InfSeq[A](override val head: A, fun: A => A)
extends LinearSeq[A] with GenericTraversableTemplate[A, List] {
  override def companion: GenericCompanion[List] = List

  def apply(idx: Int): A = {
    if(idx < 0) throw new IndexOutOfBoundsException(idx.toString)
    var res = head
    var i   = idx
    while(i > 0) {
      res = fun(res)
      i  -= 1
    }
    res
  }

  def length            = Int.MaxValue  // ?
  override def isEmpty  = false  
  override def tail     = InfSeq(fun(head), fun)
  override def toString = take(4).mkString("InfSeq(", ",", ",...)")
}

Ex.

val i = InfSeq[Int](2, _ * 2 + 3)
i.take(4).foreach(println)

Очевидно, что это еще не решает функциональность map, filter и т. д. Но если вы осторожно используете .view, все будет в порядке:

val j = i.view.map(_ * 0.5)
j.take(4).foreach(println)
person 0__    schedule 12.01.2013