Нестрогая, неизменяемая, не memoized Бесконечная серия в Scala
Мне нужен бесконечный нестрогий ряд x 1, x 2, x 3..., что я могу работать с одним элементом в то время, не запоминая результаты, чтобы поддерживать постоянное использование памяти. Для специфичности позвольте сказать, что это серия целых чисел (например, натуральные числа, нечетные числа, простые числа), хотя этот вопрос может относиться к более общим типам данных.
Самый простой способ работы с бесконечными списками - использовать объект Scala Stream
. Общая идиома заключается в том, чтобы написать функцию, которая возвращает 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
идиомы.)
Однако потоки запоминают их результаты, делая их требования к памяти непостоянными и потенциально очень большими. В документации говорится о том, что memoization можно избежать, если вы не держитесь за голову Stream
, но на практике это может быть сложно, Я могу реализовать бесконечный код списка, в котором я не думаю, что я держусь за любые потоковые заголовки, но если у него все еще есть неограниченные требования к памяти, я должен выяснить, является ли проблема в том, что я обрабатываю свои потоки каким-то образом что действительно вызывает воспоминания, или если это что-то еще. Это может быть сложной задачей отладки и имеет запах кода, потому что я пытаюсь обмануть явно memoized структуру данных, чтобы вернуть не memoized результат.
Я хочу что-то с семантикой Stream
ожидать без memoization. Похоже, что такой объект существует в Scala. Я экспериментировал с использованием итераторов для реализации бесконечных числовых рядов, но изменчивость итераторов делает это сложным, когда вы начинаете хотеть делать операции по их пониманию. Я также попытался написать свой собственный код с нуля, но не ясно, где я должен начать (должен ли я подкласс Traversable
?), Или как избежать переопределения функциональности в map
, fold
и т.д.
Есть ли у кого-нибудь хороший пример Scala код реализации нестрого, неизменяемого, не memoized бесконечного списка?
В более общем смысле, я понимаю семантику пересекающихся, повторяющихся, последовательностей, потоков и просмотров, но тот факт, что я нахожу этот вопрос настолько досадным, заставляет меня чувствовать Я что-то недопонимаю. Мне кажется, что не строгость и не memoization являются полностью ортогональными свойствами, но Scala, по-видимому, принял конструктивное решение объединить их в Stream
и не дал простой возможности очистить их. Является ли это надзором в части Scala или существует некоторая глубокая связь между нестрогой и не-memoization, которую я пропускаю?
Я понимаю, что вопрос довольно абстрактный. Вот еще один дополнительный контекст, который связывает его с конкретной проблемой.
Я столкнулся с этой проблемой в ходе реализации генератора простых чисел, как описано в статье Мейссы О'Нилл " Подлинное сито Эратосфена", и трудно дать простой пример того, где объект 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
.) Также обратите внимание, что здесь основой для бесконечных рядов являются нечетные целые числа, но наиболее общее решение включает более сложные ряды. В конце основной функции я хочу, чтобы очередь содержала две бесконечные серии:
- 3x (коэффициенты начиная с 3) (т.е. 9,12,15...)
- 5x (коэффициенты начиная с 5) (т.е. 25,30,35...)
Вышеуказанное не компилируется, потому что odds.map(...)
возвращает Iterator
, а не NumericSeries
и, следовательно, не может быть добавлен в очередь приоритетов.
На данный момент похоже, что я пробираюсь в расширения классов коллекций, и это сложно, поэтому я хочу убедиться, что мне не нужно это делать, если это абсолютно необходимо.
Ответы
Ответ 1
EDIT: При использовании filter
или map
ниже не сохраняется тип Generator
; действительно, попытка реализовать полный "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
}
}
(Я принял предложение Rex "этот генератор" ).
val i = Generator[Int](2, _ * 2 + 3)
i.take(4).foreach(println)
val j = i.map(_ * 0.5)
j.take(4).foreach(println)
Ответ 2
Если вам просто нужно уметь переписывать свой список несколько раз, попробуйте работать с 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
, который создаст две новые копии с внутренней функцией генерации и данными в том же состоянии.
Ответ 3
Это, надеюсь, самый прямой подход. Просто создайте ленивый 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
Ответ 4
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(", ",", ",...)")
}
Исх.
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)