Варианты использования для потоков в 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

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

Другие вопросы по тегам