Варианты использования для потоков в Scala
В Scala есть класс Stream, который очень похож на итератор. В чем разница между Iterator и Stream в Scala? предлагает некоторое понимание сходства и различий между ними.
Понять, как использовать поток, довольно просто, но у меня не так много распространенных случаев, когда я бы использовал поток вместо других артефактов.
Идеи, которые у меня есть сейчас:
- Если вам нужно использовать бесконечный ряд. Но это не похоже на общий случай использования, поэтому не соответствует моим критериям. (Пожалуйста, поправьте меня, если это распространено, и у меня просто слепое пятно)
- Если у вас есть ряд данных, где каждый элемент должен быть вычислен, но вы можете захотеть использовать его несколько раз. Это слабо, потому что я мог бы просто загрузить его в список, который концептуально проще для большого подмножества разработчиков.
- Возможно, существует большой набор данных или вычислительно дорогой ряд, и существует высокая вероятность того, что необходимые вам элементы не потребуют посещения всех элементов. Но в этом случае итератор будет хорошим совпадением, если вам не нужно выполнять несколько поисков, в этом случае вы также можете использовать список, даже если он будет немного менее эффективным.
- Существует сложная серия данных, которые необходимо использовать повторно. Снова список может быть использован здесь. Хотя в этом случае оба случая будут одинаково сложны в использовании, и поток будет более подходящим, поскольку не все элементы должны быть загружены. Но опять же не так часто... или это?
Так я пропустил какие-либо большие применения? Или это предпочтение разработчика по большей части?
Спасибо
4 ответа
Основное различие между 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
также. Бесконечные списки на самом деле весьма полезны - вы просто не знаете об этом, потому что у вас их нет, поэтому вы видели алгоритмы, которые являются более сложными, чем строго необходимыми, только для работы с принудительными конечными размерами.
В дополнение к ответу Даниила, имейте в виду, что 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
по мере того, как ваши неудачи нарастают, но, по-видимому, это недорого по сравнению с вашими истинными вычислениями здесь.)
Я мог бы представить, что если вы опрашиваете какое-то устройство в режиме реального времени, Stream будет более удобным.
Подумайте о GPS-трекере, который возвращает фактическое положение, если вы спросите его. Вы не можете заранее вычислить место, где вы будете через 5 минут. Вы можете использовать его в течение нескольких минут только для актуализации пути в OpenStreetMap, или вы можете использовать его для экспедиции в течение шести месяцев в пустыне или тропическом лесу.
Или цифровой термометр или другие виды датчиков, которые многократно возвращают новые данные, пока оборудование работает и включено - еще одним примером может служить фильтр файла журнала.
Stream
это к Iterator
как immutable.List
это к mutable.List
, Любимая неизменность предотвращает класс ошибок, иногда за счет производительности.
Скаляр не защищен от этих проблем: http://article.gmane.org/gmane.comp.lang.scala.internals/2831
Как указывает Даниэль, отдавая предпочтение лени, а не строгости, можно упростить алгоритмы и упростить их составление.