Модуль 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, вы перестаёте упираться в ограничения отдельных функций: вы всегда можете спуститься на уровень свёртки и собрать ровно то поведение, что нужно, не теряя функциональной чистоты.

Проверьте себя
1. Какая функция Enum фундаментальна — через неё выражаются map, filter и sum?
AEnum.each
BEnum.reduce
CEnum.sort
DEnum.count
2. Почему длинная цепочка Enum на больших данных расточительна?
AEnum не работает с большими данными
BКаждая Enum-функция энергична: проходит коллекцию и создаёт новый список на каждом шаге
CEnum копирует данные на диск
DЭто вызывает MatchError