В функциональных языках, таких как Лиспекс, итерации и циклы обычно реализуются с помощью рекурсии. Это руководство научит вас, что такое рекурсия, и как писать собственные рекурсивные функции для решения распространенных задач.
Что такое рекурсия?
Рекурсия — это простая, но мощная идея: функция, которая вызывает сама себя.
Чтобы функция не вызывала себя бесконечно, рекурсивная функция должна иметь две ключевые части:
- Базовый случай (Base Case): Условие, которое сообщает функции, когда прекратить рекурсию.
- Рекурсивный шаг (Recursive Step): Часть, где функция вызывает саму себя, но с немного измененными входными данными, которые приближают ее к базовому случаю.
Давайте посмотрим на это в действии на классическом примере.
Пример 1: Вычисление факториала
Факториал числа n (записывается как n!) — это произведение всех положительных целых чисел до n включительно. Например, 5! = 5 * 4 * 3 * 2 * 1 = 120.
Вот рекурсивная функция на Лиспексе для вычисления факториала:
(define (factorial n)
(if (<= n 1)
1
(* n (factorial (- n 1)))))
(factorial 5) ; ⇒ 120
Как это работает
Когда мы вызываем (factorial 3), вот что делает интерпретатор:
3 <= 1? Нет. Следовательно, вычисляем(* 3 (factorial 2)).- Для этого нужно вычислить
(factorial 2).2 <= 1? Нет. Следовательно, вычисляем(* 2 (factorial 1)). - Для этого нужно вычислить
(factorial 1).1 <= 1? Да. Он достигает базового случая и возвращает1. - Теперь он может решить шаг 2:
(* 2 1)равно2. - Теперь он может решить шаг 1:
(* 3 2)равно6.
Конечный результат — 6.
Пример 2: Рекурсия по списку
Рекурсия — это основной способ "пройтись циклом" по списку в Лиспексе. Распространенный шаблон — обработать первый элемент, а затем рекурсивно вызвать функцию для остальной части списка.
Давайте напишем функцию для суммирования всех чисел в списке.
(define (sum-list lst)
(if (empty? lst)
0
(+ (list-first lst) (sum-list (list-rest lst)))))
(sum-list (list 10 20 30)) ; ⇒ 60
Как это работает
sum-listвызывается со списком(list 10 20 30).- Список не пуст, поэтому вычисляется
(+ 10 (sum-list (list 20 30))). sum-listвызывается со списком(list 20 30).- Список не пуст, поэтому вычисляется
(+ 20 (sum-list (list 30))). sum-listвызывается со списком(list 30).- Список не пуст, поэтому вычисляется
(+ 30 (sum-list (list))). sum-listвызывается с пустым списком(). Он достигает базового случая и возвращает0.- Затем результаты "всплывают":
(+ 30 0)равно30,(+ 20 30)равно50, и, наконец,(+ 10 50)равно60.
Замечание о хвостовой рекурсии
При очень глубокой рекурсии (например, (factorial 10000)) простой рекурсивный шаблон может иногда приводить к ошибке, называемой "переполнение стека". Для решения этой проблемы можно использовать технику, известную как хвостовая рекурсия (tail recursion).
Функция является хвостово-рекурсивной, если рекурсивный вызов — это самое последнее действие, которое она выполняет. Это позволяет интерпретатору Лиспекс выполнить оптимизацию (оптимизация хвостового вызова, Tail Call Optimization), чтобы избежать ошибок переполнения стека. Это часто достигается с помощью переменной-"аккумулятора".
Вот хвостово-рекурсивная версия нашей функции факториала:
(define (factorial n)
(let ((factorial-helper (lambda (n acc)
(if (<= n 1)
acc
(factorial-helper (- n 1) (* acc n))))))
(factorial-helper n 1)))
(factorial 5) ; ⇒ 120 (хвостовая рекурсия)
Заключение
Рекурсия — это фундаментальная концепция в Лиспексе. Понимая базовый случай и рекурсивный шаг, вы можете выполнять любые виды итераций или циклов чистым, функциональным способом.
Теперь попробуйте написать свою собственную рекурсивную функцию! Например, сможете ли вы написать функцию my-length, которая рекурсивно вычисляет длину списка?