Как реализовать stream-подобную обработку в Scala?

В Scala можно реализовать stream-подобную обработку данных несколькими способами, каждый из которых ориентирован на ленивую (lazy) обработку последовательностей. Такой подход позволяет работать с потенциально бесконечными структурами или сэкономить ресурсы при обработке больших объёмов данных, загружая в память только необходимые элементы. Основными средствами для stream-подобной обработки являются LazyList, Iterator, View, Stream (в Scala 2), а также сторонние библиотеки, такие как FS2 и Akka Streams.

1. LazyList (в Scala 2.13+)

LazyList — это лениво вычисляемая последовательность. Элементы вычисляются по мере запроса, а результат кешируется.

Пример:

val naturals: LazyList\[Int\] = LazyList.from(1)
val evens = naturals.filter(_ % 2 == 0)
val firstFive = evens.take(5).toList // List(2, 4, 6, 8, 10)

Особенности:

  • Ленивый.

  • Потенциально бесконечный.

  • Подходит для вычислений с рекурсией.

2. Iterator

Iterator также реализует ленивую обработку, но без кеширования. Каждый элемент удаляется после чтения.

Пример:

val data = Iterator.from(1)
val result = data
.filter(_ % 3 == 0)
.map(_ \* 2)
.take(5)
.toList // List(6, 12, 18, 24, 30)

Различие с LazyList:

  • Iterator разрушаемый, одноразовый.

  • Подходит для чтения из файлов, сетевых потоков.

3. View

View предоставляет ленивую обёртку над коллекцией. Позволяет не создавать промежуточные коллекции.

Пример:

val numbers = (1 to 1000000).view
val processed = numbers
.filter(_ % 10 == 0)
.map(_ \* 2)
processed.take(5).toList // List(20, 40, 60, 80, 100)

Преимущества:

  • Позволяет сохранить исходную коллекцию и использовать цепочки операций без лишних аллокаций.

  • view сохраняет тип коллекции, но ленивость делает её более эффективной при большом объеме.

4. Stream (только в Scala 2.x)

Stream — предшественник LazyList. Работает аналогично, но в Scala 2.13 и выше заменён на LazyList.

Пример:

def fibs: Stream\[BigInt\] = {
def loop(a: BigInt, b: BigInt): Stream\[BigInt\] = a #:: loop(b, a + b)
loop(0, 1)
}
fibs.take(10).toList // List(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)

5. FS2 (Functional Streams for Scala)

Это мощная библиотека для асинхронной, потоковой и функциональной обработки данных. Основана на cats-effect.

Пример:

import cats.effect._
import fs2._
val stream = Stream.emits(1 to 100).covary\[IO\]
.filter(_ % 2 == 0)
.map(_ \* 10)
.take(5)
stream.compile.toList.unsafeRunSync()
// List(20, 40, 60, 80, 100)

Преимущества:

  • Асинхронность, отмена, backpressure.

  • Отлично подходит для IO-bound задач.

6. Akka Streams

Akka Streams — реализация реактивного стриминга с управлением потоком (backpressure).

Пример (на базе Akka):

import akka.actor.ActorSystem
import akka.stream.scaladsl._
implicit val system = ActorSystem("StreamSystem")
val source = Source(1 to 100)
val flow = Flow\[Int\].filter(_ % 2 == 0).map(_ \* 2)
val sink = Sink.foreach\[Int\](println)
source.via(flow).runWith(sink)

Преимущества:

  • Потоковая архитектура.

  • Поддержка реактивных систем.

  • Возможность распределённой обработки.

Сравнение:

Подход Ленивость Потоковость Асинхронность Кеширование Применение
LazyList Да Нет Нет Да Ленивые последовательности
--- --- --- --- --- ---
Iterator Да Нет Нет Нет Однократное чтение
--- --- --- --- --- ---
View Да Нет Нет Нет Эффективная обработка коллекций
--- --- --- --- --- ---
FS2 Да Да Да Управляется Стриминг в FP-приложениях
--- --- --- --- --- ---
Akka Streams Да Да Да Управляется Реактивные, масштабируемые стримы
--- --- --- --- --- ---

Заключение о композиции:

Все ленивые подходы в Scala позволяют строить цепочки трансформаций:

val processed = LazyList.from(1)
.map(_ \* 3)
.filter(_ % 2 == 1)
.take(10)

Они следуют функциональному стилю: чистые функции, неизменяемость и декларативность. Это позволяет строить читаемый и эффективный код даже при работе с бесконечными или тяжёлыми источниками данных.