Варианты использования для потоков в Scala

В Scala есть класс Stream, который очень похож на итератор. В разделе Разница между Iterator и Stream в Scala? дает некоторое представление о сходствах и различиях между ними.

Увидеть, как использовать поток, довольно просто, но у меня нет очень многих обычных вариантов использования, где я бы использовал поток вместо других артефактов.

Идеи, которые у меня есть прямо сейчас:

  • Если вам нужно использовать бесконечную серию. Но это не похоже на обычный случай использования, поэтому он не соответствует моим критериям. (Пожалуйста, поправьте меня, если это распространено, и у меня просто слепое место).
  • Если у вас есть серия данных, в которых каждый элемент должен быть вычислен, но вы можете повторно использовать его несколько раз. Это слабо, потому что я могу просто загрузить его в список, который концептуально проще отслеживать для большого подмножества популяции разработчиков.
  • Возможно, существует большой набор данных или дорогостоящая серия, и существует высокая вероятность того, что нужные вам элементы не потребуют посещения всех элементов. Но в этом случае Итератор будет хорошим совпадением, если вам не нужно выполнять несколько поисков, и в этом случае вы могли бы использовать список, даже если он будет немного менее эффективным.
  • Существует сложная серия данных, которые необходимо повторно использовать. Здесь можно использовать список. Хотя в этом случае оба случая были бы одинаково сложны в использовании, и Stream был бы более подходящим, поскольку не все элементы должны быть загружены. Но опять же не так часто... или это?

Так что я пропустил какие-то большие возможности? Или это предпочтение разработчика по большей части?

Спасибо

Ответ 1

Основное различие между a Stream и Iterator заключается в том, что последнее является изменчивым и, если можно так выразиться, однократным, в то время как первое не является. Iterator имеет лучший размер памяти, чем Stream, но тот факт, что он изменен, может быть неудобным.

Возьмите этот классический генератор простых чисел, например:

def primeStream(s: Stream[Int]): Stream[Int] =
  Stream.cons(s.head, primeStream(s.tail filter { _ % s.head != 0 }))
val primes = primeStream(Stream.from(2))

Его также легко записать с помощью Iterator, но Iterator не сохранит вычисляемые простые числа.

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

Что же касается дорогостоящих вычислений/бесконечных списков, то это можно сделать и с помощью Iterator. Бесконечные списки на самом деле весьма полезны - вы просто не знаете этого, потому что у вас его нет, поэтому вы видели алгоритмы, которые сложнее, чем строго необходимы, чтобы справиться с принудительными конечными размерами.

Ответ 2

В дополнение к ответам Даниэля, имейте в виду, что Stream полезен для коротких замыканий. Например, предположим, что у меня есть огромный набор функций, которые принимают String и возвращают Option[String], и я хочу продолжать выполнять их, пока один из них не будет работать:

val stringOps = List(
  (s:String) => if (s.length>10) Some(s.length.toString) else None ,
  (s:String) => if (s.length==0) Some("empty") else None ,
  (s:String) => if (s.indexOf(" ")>=0) Some(s.trim) else None
);

Ну, я, конечно, не хочу исполнять весь список, и на List нет удобного метода, который говорит: "рассматривайте их как функции и выполняйте их, пока один из них не возвращает что-то другое, кроме None". Что делать? Возможно, это:

def transform(input: String, ops: List[String=>Option[String]]) = {
  ops.toStream.map( _(input) ).find(_ isDefined).getOrElse(None)
}

Это принимает список и рассматривает его как Stream (который фактически ничего не оценивает), а затем определяет новый Stream, который является результатом применения функций (но это еще ничего не оценивает), затем ищет первый, который определен - и здесь, по волшебству, он оглядывается назад и понимает, что он должен применять карту и получать нужные данные из исходного списка, а затем разворачивает ее с Option[Option[String]] до Option[String] с помощью getOrElse.

Вот пример:

scala> transform("This is a really long string",stringOps)
res0: Option[String] = Some(28)

scala> transform("",stringOps)
res1: Option[String] = Some(empty)

scala> transform("  hi ",stringOps)
res2: Option[String] = Some(hi)

scala> transform("no-match",stringOps)
res3: Option[String] = None

Но работает ли это? Если положить println в наши функции, чтобы мы могли определить, вызваны ли они, получим

val stringOps = List(
  (s:String) => {println("1"); if (s.length>10) Some(s.length.toString) else None },
  (s:String) => {println("2"); if (s.length==0) Some("empty") else None },
  (s:String) => {println("3"); if (s.indexOf(" ")>=0) Some(s.trim) else None }
);
// (transform is the same)

scala> transform("This is a really long string",stringOps)
1
res0: Option[String] = Some(28)

scala> transform("no-match",stringOps)                    
1
2
3
res1: Option[String] = None

(Это означает, что реализация Scala 2.8; 2.7 иногда будет превышать одну, к сожалению. И обратите внимание, что вы накапливаете длинный список None по мере того, как ваши сбои накапливаются, но, по-видимому, это недорого по сравнению с вашим истинным вычислением здесь.)

Ответ 3

Я мог себе представить, что если вы опросите какое-то устройство в режиме реального времени, Stream станет более удобным.

Подумайте о GPS-трекере, который возвращает фактическую позицию, если вы спросите об этом. Вы не можете предварительно скомпоновать место, где вы будете находиться через 5 минут. Вы можете использовать его в течение нескольких минут только для реализации пути в OpenStreetMap, или вы можете использовать его для экспедиции в течение шести месяцев в пустыне или в тропическом лесу.

Или цифровой термометр или другие типы датчиков, которые многократно возвращают новые данные, пока аппаратное обеспечение живое и включено - фильтр журнальных файлов может быть другим примером.

Ответ 4

Stream соответствует Iterator, поскольку immutable.List соответствует mutable.List. Благоприятная неизменность предотвращает класс ошибок, иногда за счет производительности.

сам scalac не застрахован от этих проблем: http://article.gmane.org/gmane.comp.lang.scala.internals/2831

Как отмечает Даниил, предпочтение лени от строгости может упростить алгоритмы и упростить их составление.