В чём разница между Enum и Stream?

В Elixir модули Enum и Stream предоставляют мощные инструменты для работы с коллекциями, однако между ними есть важные различия, которые затрагивают производительность, обработку данных и способ исполнения кода. Главное различие — жадность (Enum) и ленивость (Stream).

Enum

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

Пример:

Enum.map(\[1, 2, 3\], fn x -> x \* 2 end)
\# => \[2, 4, 6\]

Вся коллекция [1, 2, 3] обрабатывается целиком, и на выходе сразу возвращается результат.

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

  • Все операции выполняются сразу и целиком.

  • Результат — новый список (или коллекция).

  • Требует загрузки всей коллекции в память.

  • Идеален для небольших наборов данных.

  • Часто используется в пайпах (pipeline):

\[1, 2, 3\]
|> Enum.map(&(&1 \* 2))
|> Enum.filter(&rem(&1, 3) == 0)

Примеры функций:

  • Enum.map/2, Enum.filter/2, Enum.reduce/3

  • Enum.any?/2, Enum.all?/2

  • Enum.sort/2, Enum.reverse/1

  • Enum.take/2, Enum.drop/2

Stream

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

Пример:

stream = Stream.map(\[1, 2, 3\], fn x -> x \* 2 end)
\# ничего не происходит
Enum.to_list(stream)
\# => \[2, 4, 6\]

Функция Stream.map/2 возвращает ленивую трансформацию, которая применяется только при вызове Enum.to_list/1 или любой другой "жадной" функции.

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

  • Не выполняется сразу — только при потреблении.

  • Не требует загрузки всей коллекции в память.

  • Подходит для больших, потенциально бесконечных источников данных.

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

  • Идеален при работе с файлами, сокетами, внешними API.

Примеры функций:

  • Stream.map/2, Stream.filter/2, Stream.reject/2

  • Stream.take/2, Stream.drop/2

  • Stream.cycle/1, Stream.iterate/2

  • Stream.unfold/2, Stream.resource/3 — для создания кастомных потоков.

Пример сравнения работы

С Enum:

Enum.map(1..1_000_000, fn x -> x \* 2 end)
\# создаётся список из миллиона значений, каждый умножается сразу

С Stream:

1..1_000_000
|> Stream.map(&(&1 \* 2))
|> Stream.filter(&(rem(&1, 3) == 0))
|> Enum.take(5)
\# обрабатываются только первые 5 нужных значений

Здесь Stream не проходит всю коллекцию, а только до тех пор, пока не получит 5 подходящих значений. Это значительно экономит память и ресурсы.

Когда использовать Enum и когда Stream

Ситуация Enum Stream
Маленькие коллекции Возможно, но избыточно
--- --- ---
Нужен быстрый, прямой результат Нет
--- --- ---
Большие коллекции ⚠️ Много памяти
--- --- ---
Обработка файлов, внешних источников
--- --- ---
Композиция операций без промежуточных структур
--- --- ---
Работа с бесконечными потоками данных
--- --- ---

Пример с чтением файла

\# Stream - построчная обработка
File.stream!("large.txt")
|> Stream.map(&String.trim/1)
|> Stream.filter(&(&1 != ""))
|> Enum.take(10)

Здесь File.stream!/1 возвращает Stream, позволяя читать файл построчно, не загружая его целиком в память.

Взаимосвязь Enum и Stream

  • Stream используется для построения цепочки трансформаций.

  • Enum используется для завершения и получения результата.

Почти всегда Stream завершается вызовом Enum.to_list/1, Enum.take/2, Enum.reduce/3 и других функций из Enum.

Таким образом, оба модуля предоставляют мощные и гибкие инструменты, и выбор между ними зависит от размера данных, потребности в производительности и необходимости в ленивом выполнении.