Что такое рекурсия и как с её помощью заменить циклы?
Рекурсия — это механизм, при котором функция вызывает саму себя для решения подзадачи. Это основной способ реализации повторяющихся операций в функциональных языках программирования, таких как Elixir, где отсутствуют привычные императивные конструкции for, while и переменные-счётчики. Вместо них используются рекурсивные вызовы и сопоставление с образцом (pattern matching).
Что происходит при рекурсии
Рекурсивная функция должна:
-
иметь условие завершения (базовый случай) — чтобы остановить вызовы;
-
вызывать себя с новым, изменённым аргументом — для продвижения к завершению;
-
возвращать результат — или аккумулировать его через параметры.
Пример простейшей рекурсии: сумма списка
defmodule Utils do
def sum(\[\]), do: 0
def sum(\[head | tail\]), do: head + sum(tail)
end
Разбор:
-
Базовый случай — пустой список [], сумма которого равна 0.
-
Рекурсивный случай — добавление первого элемента (head) к сумме оставшихся (tail).
-
На каждом шаге список уменьшается, приближаясь к базовому случаю.
Как рекурсия заменяет цикл
В императивных языках:
sum = 0
for i in list do
sum = sum + i
end
В функциональных:
Utils.sum(list)
Всё, что делал цикл — шаг за шагом аккумулировал значение — теперь делает рекурсивная функция.
Хвостовая рекурсия
Elixir (и BEAM) поддерживает хвостовую рекурсию (tail recursion) — оптимизацию, при которой функция не создает новый стек вызова, если последний шаг — это вызов самой себя. Это позволяет писать рекурсивные функции, не боясь переполнения стека.
Пример с хвостовой рекурсией:
defmodule Utils do
def sum_tail(list), do: sum_tail(list, 0)
defp sum_tail(\[\], acc), do: acc
defp sum_tail(\[head | tail\], acc), do: sum_tail(tail, acc + head)
end
Здесь acc — аккумулятор, который накапливает результат. После последнего вызова стек очищается, не накапливая слои.
Пример: подсчёт длины списка
def length(\[\]), do: 0
def length(\[_ | tail\]), do: 1 + length(tail)
Аналог Enum.count/1, но через рекурсию.
Пример: map — применить функцию к каждому элементу списка
def map(\[\], \_func), do: \[\]
def map(\[head | tail\], func), do: \[func.(head) | map(tail, func)\]
Аналог Enum.map/2.
Пример: фильтрация значений
def filter(\[\], \_func), do: \[\]
def filter(\[head | tail\], func) do
if func.(head) do
\[head | filter(tail, func)\]
else
filter(tail, func)
end
end
Работает как Enum.filter/2.
Рекурсия с несколькими аргументами
Когда нужно обрабатывать два списка одновременно:
def zip(\[\], \[\]), do: \[\]
def zip(\[h1 | t1\], \[h2 | t2\]), do: \[{h1, h2} | zip(t1, t2)\]
Рекурсивная реализация reduce
def reduce(\[\], acc, \_func), do: acc
def reduce(\[head | tail\], acc, func), do: reduce(tail, func.(head, acc), func)
Почему рекурсия — основной способ повторений в Elixir
-
Отсутствие мутабельных переменных исключает возможность изменять значения внутри цикла.
-
Отказ от императивных конструкций делает циклы невозможными.
-
Рекурсия позволяет описывать логику без состояния.
-
Сопоставление с образцом делает разбор структуры данных простым и безопасным.
Где особенно удобна рекурсия
-
При обработке вложенных структур (списки списков, деревья)
-
При реализации собственных итераторов
-
Для построения чистых функций без побочных эффектов
Рекурсия позволяет выразить повторяющуюся логику элегантно, структурировано и безопасно. С опытом её использование становится привычным способом мыслить в Elixir и других функциональных языках.