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