Это предыдущая редакция документации. Чтобы пошагово вычислить сумму списка, откройте актуальное руководство по рекурсии. Правила хвостовых вызовов описаны в разделе «Правильные хвостовые вызовы и рекурсия».
В функциональных языках, таких как Lispex, итерации часто реализуются с помощью рекурсии. Это руководство показывает, что такое рекурсия и как писать рекурсивные функции для типичных задач.
Что такое рекурсия?
Рекурсия это простая, но мощная идея: функция, которая вызывает сама себя.
Чтобы функция не вызывала себя бесконечно, рекурсивная функция должна иметь две ключевые части:
- Базовый случай (Base Case): Условие, которое сообщает функции, когда прекратить рекурсию.
- Рекурсивный шаг (Recursive Step): Часть, где функция вызывает саму себя, но с немного измененными входными данными, которые приближают ее к базовому случаю.
Давайте посмотрим на это в действии на классическом примере.
Пример 1: Вычисление факториала
Факториал числа n (записывается как n!) это произведение всех положительных целых чисел до n включительно. Например, 5! = 5 * 4 * 3 * 2 * 1 = 120.
Вот рекурсивная функция на Lispex для вычисления факториала:
(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: Рекурсия по списку
Рекурсия это основной способ пройти по списку в Lispex. Распространённый шаблон: обработать первый элемент, а затем рекурсивно вызвать функцию для остальной части списка.
Давайте напишем функцию для суммирования всех чисел в списке.
(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).
Функция является хвостово-рекурсивной, если рекурсивный вызов это самое последнее действие, которое она выполняет. Это позволяет интерпретатору Lispex выполнить оптимизацию хвостового вызова и избежать ошибок переполнения стека. Часто для этого используют переменную-аккумулятор.
Вот хвостово-рекурсивная версия нашей функции факториала:
(define (factorial n)
(letrec ((factorial-helper (lambda (n acc)
(if (<= n 1)
acc
(factorial-helper (- n 1) (* acc n))))))
(factorial-helper n 1)))
(factorial 5) ; ⇒ 120 (хвостовая рекурсия)Заключение
Рекурсия это базовый приём в Lispex. Понимая базовый случай и рекурсивный шаг, вы можете описывать повторение чистым функциональным способом.
Теперь попробуйте написать свою собственную рекурсивную функцию! Например, сможете ли вы написать функцию my-length, которая рекурсивно вычисляет длину списка?