Как работает Enum.reduce/3 и где он применяется?

Enum.reduce/3 — это функция в Elixir, которая используется для свёртки (агрегации, накопления) значений в перечисляемой коллекции. Она последовательно проходит по каждому элементу коллекции, применяет переданную функцию к текущему элементу и аккумулятору (промежуточному значению), возвращает новое значение аккумулятора, и затем передаёт его на следующую итерацию. После завершения всех итераций возвращается итоговое значение аккумулятора.

Сигнатура

Enum.reduce(enumerable, acc, fun)
  • enumerable — перечисляемая коллекция (список, Map, Range, Stream и др.).

  • acc — начальное значение аккумулятора.

  • fun — функция, принимающая два аргумента: element и acc.

Простой пример

Enum.reduce(\[1, 2, 3, 4\], 0, fn x, acc -> acc + x end)
\# => 10

Процесс:

  • acc = 0, x = 1 → acc = 1

  • acc = 1, x = 2 → acc = 3

  • acc = 3, x = 3 → acc = 6

  • acc = 6, x = 4 → acc = 10

Альтернативная запись с захватом функции

Enum.reduce(\[1, 2, 3\], 0, &+/2)
\# => 6

&+/2 — сокращённая форма передачи анонимной функции сложения.

Пример с аккумуляцией строки

Enum.reduce(\["a", "b", "c"\], "", fn x, acc -> acc <> x end)
\# => "abc"

Применение для преобразования структур

Построение Map из списка кортежей:

Enum.reduce(\[{:a, 1}, {:b, 2}\], %{}, fn {k, v}, acc ->
Map.put(acc, k, v)
end)
\# => %{a: 1, b: 2}

Подсчёт количества элементов:

Enum.reduce(\[:a, :b, :a\], %{}, fn elem, acc ->
Map.update(acc, elem, 1, &(&1 + 1))
end)
\# => %{a: 2, b: 1}

Применение в вычислениях и бизнес-логике

Расчёт среднего значения:

list = \[4, 8, 15, 16, 23, 42\]
{sum, count} =
Enum.reduce(list, {0, 0}, fn x, {sum, count} ->
{sum + x, count + 1}
end)
avg = sum / count
\# => 18.0

Использование с Map, Range, Stream

Enum.reduce(%{a: 1, b: 2}, 0, fn {\_k, v}, acc -> acc + v end)
\# => 3
Enum.reduce(1..5, 1, fn x, acc -> acc \* x end)
\# => 120
stream = Stream.cycle(\[1, 2, 3\])
Enum.reduce(Enum.take(stream, 5), 0, fn x, acc -> acc + x end)
\# => 10

Отличие от других Enum функций

  • Enum.map/2 возвращает новую коллекцию.

  • Enum.reduce/3 возвращает одно агрегированное значение.

  • Enum.each/2 используется для побочных эффектов и не возвращает аккумулятор.

  • Enum.reduce_while/3 позволяет прервать цикл досрочно, что reduce/3 не делает.

Использование в императивных задачах

Elixir не поддерживает явные циклы как в других языках (for, while), поэтому Enum.reduce/3 — один из способов заменить счётчики, аккумуляторы и циклы.

Пример: подсчёт символов в строке

str = "elixir"
chars = String.graphemes(str)
Enum.reduce(chars, %{}, fn ch, acc ->
Map.update(acc, ch, 1, &(&1 + 1))
end)
\# => %{"e" => 1, "l" => 1, "i" => 2, "x" => 1, "r" => 1}

Пример распределения значений

Допустим, есть список количеств, и нужно распределить 100 единиц пропорционально:

quantities = \[2, 3, 5\]
total_qty = Enum.sum(quantities)
sum_to_distribute = 100
Enum.reduce(quantities, {\[\], 0}, fn qty, {result, acc_sum} ->
part = round(qty / total_qty \* sum_to_distribute)
{\[part | result\], acc_sum + part}
end)
\# => {\[20, 30, 50\], 100}

Вложенные редьюсы

Можно использовать Enum.reduce внутри Enum.reduce:

matrix = \[\[1, 2\], \[3, 4\], \[5, 6\]\]
Enum.reduce(matrix, \[\], fn row, acc ->
row_sum = Enum.reduce(row, 0, &+/2)
\[row_sum | acc\]
end)
\# => \[9, 7, 3\]

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