
8. Рекурсія.
Основна ідея рекурсивного визначення у тому, що функцію можна з допомогою рекуррентных формул зводити до деяким початковим значенням, до раніше певним функцій або до самої обумовленою функції, але з більш «простими» аргументами. Обчислення такий функції закінчується той момент, як його зводиться до відомим початковим значениям.
Рекурсивна процедура, по-перше містить за крайнього заходу одну термінальну гілка і умова закінчення. По-друге, коли процедура доходить до рекурсивної галузі, то функціонуючий процес припиняється, і новий такий процес запускається спочатку, але вже новий рівень. Перерваний процес якимось чином запам'ятовується. Він чекати, і почне виконуватися лише по закінченні нового процесу. Натомість, новий процес може призупинитися, очікувати й т. д.
Говоритимемо про рекурсії за значенням і рекурсії по аргументів. У першому випадку виклик є вираженням, визначальним результат функції. У другому - як результату функції повертається значення деякою інший функції і рекурсивний виклик бере участь у обчисленні аргументів цієї функції. Аргументом рекурсивного виклику то, можливо знову рекурсивний виклик і такі викликів то, можливо много.
Розглянемо такі форми рекурсії: проста рекурсія; паралельна рекурсія; взаємна рекурсия.
Рекурсія називається простий, якщо виклик функції є у деякою галузі лише одне раз. Простий рекурсії в процедурному програмуванні відповідає звичайний цикл.
Наприклад напишемо функцію обчислення чисел Фібоначчі (F(1)=1; F(2)=1; F(n)=F(n-1)+F(n-2) при n>2):
(DEFUN FIB (N)
(IF (> N 0)
(IF (OR N=1 N=2) 1
(+ (FIB (- N 1)) (FIB (- N 2))))
‘ОШИБКА_ВВОДА))
Рекурсію називають паралельної, якщо вона зустрічається одночасно у кількох аргументах функції:
(DEFUN f ...
...(g ... (f ...) (f ...) ...)
...)
Розглянемо використання паралельної рекурсії з прикладу перетворення списковою структури в однорівневий список:
(DEFUN PREOBR (L)
(COND
((NULL L) NIL)
((ATOM L) (CONS (CAR L) NIL))
(T (APPEND
(PREOBR (CAR L))
(PREOBR (CDR L))))))
Рекурсія є взаємної між двома і більше функціями, якщо вони викликають друг друга:
(DEFUN f ...
...(g ...) ...)
(DEFUN g ...
...(f ...) ...)
Наприклад напишемо функцію звернення чи дзеркального відображення в вигляді двох взаємно рекурсивних функцій наступним образом:
(DEFUN obr (l)
(COND ((ATOM l) l)
(T (per l nil))))
(DEFUN per (l res)
(COND ((NULL l) res)
(T (per (CDR l)
(CONS (obr (CAR l)) res)))))
9. Функція як дані.
Якщо ви вже знайомі з якоюсь алголоподібною мовою типу Сі або Перла, то поняття функції в Ліспі може здатися дивним, але воно значно ближче до математичного явищу функції, ніж «функції» Сі, які вірніше було б назвати більш процедурними. Функція в Ліспі є однозначне відображення множини початкових даних на множину її значень. У функції може бути довільно багато аргументів, - від нуля до будь-якого кінцевого числа, - але обов'язково має бути хоча б одне значення. Звичайна для багатьох мов префіксная запис виклику функцій:
plus(2,2)
трохи видозмінюється в силу відсутності синтаксису: ідентифікатор ("ім'я" функції, спецоператора або макровиклику) стає головним елементом списку:
(plus 2 2)
У чистому функціональному Ліспі (що є строгою підмножиною будь-якого з діалектів) функції не повинні володіти побучним ефектом, тобто змінювати значення змінних з нелокальним станом. В іншому випадку, при декількох викликах однієї функції можуть бути отримані різні значення, що заплутує будь-яку складну програмну систему, і чого прагне уникнути функціональне програмування.
Визначення
функції засноване на лямбда-численні.
Вихідний варіант запису лямбда-вирази,
запропонований його автором Черчем,
виглядає як
.
Лісп-запис виглядає так: (lambda (x1 x2 ...) fn).
x1, x2 ... - Вільні параметри, і, так, можуть
бути безболісно замінені на інші без
зміни значення лямбда-вирази.
Наприклад, суму квадратів на мові лямбда-виразів можна визначити як (lambda (xy) (+ (* xx) (* yy))). Виклик лямбда-функції з конкретними значеннями ((lambda (xy) (+ (* xx) (* yy))) 2 3) => (+ (* 2 2) (* 3 3)) => 13 може бути зроблений раз , при бажанні викликати таку функцію ще раз необхідно повторити запис ще раз. Це незручно. Було б зручніше написати щось типу (сума-квадратів 2 3). Це можливо, якщо ви попередньо визначте функцію сума-квадратів
(defun сумма-квадратов (x y)
((lambda (a b) (+ (* a a) (* b b)))
x y))
… але не лякайтеся: для зручності цей запис може бути скорочений до
(defun сумма-квадратов (x y)
(+ (* x x) (* y y)))
((lambda (x)
Одна з переваг таких незвичних особливостей мови Лісп в тому, що ви можете написати щось типу
((lambda (y)
(list x y))
'second))
'first)
Тут лямбда-вираз є тілом іншої лямбда-виразу. Воно так само може бути одним з його аргументів.
Подивитися раніше заданий визначення функції можна за допомогою функції symbol-function:
>> (symbol-function 'сумма-квадратов)
<< #<function сумма-квадратов (x y) (declare (system::in-defun сумма-квадратов))
(block сумма-квадратов (+ (* x x) (* y y)))>
Штрих перед назвою функції поставлений невипадково. В Ліспі будь-яке ім'я відповідає деякому символу. Ім'я функції - не виняток, тобто значення символу «сума-квадратів» - не визначення функції; воно невизначене, поки не буде присвоєно вручну (наприклад, через setq.) Штрих - це скорочений запис спецоператора quote, що повідомляє інтерпретатору, що потрібно вивести ім'я даного символу без обчислення його значення.
>> (symbol-function сумма-квадратов) ; это выдаст ошибку: переменной сумма-квадратов не присвоено значение
>> (symbol-function (quote сумма-квадратов)) ; это правильно, но долго
>> (symbol-function 'сумма-квадратов) ; так лучше всего
Якщо всі аргументи лямбда-вирази не повинні обчислюватися, то зручно використовувати nlambda замість lambda.
quote'е протиставлений eval, примусово обчислює значення виразу.
>> (quote сумма-квадратов) => сумма-квадратов
>> (eval (quote сумма-квадратов)) => ошибка!
>> (quote (eval (quote сумма-квадратов))) => (eval 'sum)
В останньому випадку квотується не тільки сума-квадратів, але і сам eval!