Варианты использования для потоков в 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
Как отмечает Даниил, предпочтение лени от строгости может упростить алгоритмы и упростить их составление.