Модуль Enum: обход коллекций
Модуль Enum — рабочая лошадь обработки коллекций. Map, filter, reduce — всё здесь, с единым интерфейсом для списков, map'ов и диапазонов.
90% обработки данных в Elixir — это цепочка функций Enum в pipe. Освоить Enum — значит писать на Elixir идиоматично.
Три кита функционального обхода — map, filter, reduce:
Enum.map([1, 2, 3], fn x -> x * 2 end) # [2, 4, 6]
Enum.filter([1, 2, 3, 4], fn x -> rem(x,2)==0 end) # [2, 4]
Enum.reduce([1, 2, 3, 4], 0, fn x, acc -> x + acc end) # 10
В pipe это читается как поток преобразований:
1..10
|> Enum.filter(&(rem(&1, 2) == 0)) # [2, 4, 6, 8, 10]
|> Enum.map(&(&1 * &1)) # квадраты
|> Enum.sum() # 220
Enum работает с любым «перечислимым» — списками, диапазонами 1..10, map'ами:
Enum.map(%{a: 1, b: 2}, fn {k, v} -> {k, v * 10} end)
# => [a: 10, b: 20]
Как работает под капотом (BEAM)
Enum опирается на протокол Enumerable: любая структура, реализующая его, умеет «отдавать элементы по одному». Сам reduce — фундаментальная операция, через которую выражаются и map, и filter, и sum. Важно: функции Enum энергичные (eager) — каждая создаёт новую полную коллекцию. Цепочка из трёх Enum-шагов трижды пройдёт данные и трижды выделит память под промежуточные списки. Для одного-двух шагов это незаметно, но на больших данных это повод вспомнить про ленивый Stream.
Enum.reduce — основа всего:
[1,2,3,4] acc=0
1 -> acc=0+1=1
2 -> acc=1+2=3
3 -> acc=3+3=6
4 -> acc=6+4=10 => результат 10
Та же идея на Python ▶
В Python это map/filter/reduce и генераторы списков.
from functools import reduce
print(list(map(lambda x: x * 2, [1, 2, 3]))) # [2, 4, 6]
print([x for x in [1,2,3,4] if x % 2 == 0]) # [2, 4]
print(reduce(lambda acc, x: acc + x, [1,2,3,4], 0)) # 10
# Конвейер как Enum в pipe
evens = (x for x in range(1, 11) if x % 2 == 0)
squares = (x * x for x in evens)
print(sum(squares)) # 220
# Обход словаря с трансформацией значений
data = {"a": 1, "b": 2}
print({k: v * 10 for k, v in data.items()}) # {'a': 10, 'b': 20}
Частые ошибки
- Длинные цепочки Enum на больших данных. Каждый шаг проходит коллекцию заново — на миллионах элементов это расточительно.
- Путать аргументы reduce. В
fn x, acc -> ...аккумулятор — второй параметр; перепутать порядок легко. - Ожидать порядок при обходе map. Порядок ключей map'а не гарантирован; не полагайтесь на него.
Best practices
- Стройте обработку как pipe из Enum-функций — это эталонный стиль.
- Помните:
reduce— универсален, через него выражается почти всё; используйте его, когда готовой функции нет. - Для длинных пайплайнов на больших данных переходите на
Stream(следующий урок).
Итог. Enum даёт декларативный, единообразный обход коллекций. Связка Enum + pipe — основной рабочий инструмент Elixir-программиста. Но у энергичной обработки есть цена; снять её помогает ленивый Stream.
Палитра Enum шире трёх функций
Map, filter и reduce — основа, но модуль Enum огромен, и знание его палитры экономит силы. Enum.group_by разложит коллекцию по ключам в map списков, Enum.frequencies посчитает повторения, Enum.chunk_every нарежет на пачки, Enum.zip склеит две коллекции попарно, Enum.sort_by отсортирует по вычисленному ключу. Прежде чем писать рекурсию руками, почти всегда стоит проверить, нет ли готовой функции Enum — обычно есть, и она читается яснее.
Отдельного упоминания заслуживает Enum.reduce с map'ом или кортежем в аккумуляторе — это «швейцарский нож», через который выражается любая нестандартная свёртка, когда готовой функции не нашлось: одновременный подсчёт суммы и количества, построение индекса, накопление результата с побочным состоянием. Освоив reduce, вы перестаёте упираться в ограничения отдельных функций: вы всегда можете спуститься на уровень свёртки и собрать ровно то поведение, что нужно, не теряя функциональной чистоты.