Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

книги / Функциональное программирование

..pdf
Скачиваний:
0
Добавлен:
12.11.2023
Размер:
14.84 Mб
Скачать

>(СОРТИРУЙ '(b а с) '(a b c d e)) (А В С)

Теперь рекурсия функций СОРТИРУЙ, ВСТАВЬ и РАНЬШЕ-Р образует уже трехуровневую вложенную повторяющуюся структуру.

Приведем еще функцию РАССТАВЬ, напоминающую своей структурой функцию СОРТИРУЙ и позволяющую элементы списка НОВЫЙ вставить в упорядоченный список L. Функция РАССТАВЬ повторяет процедуру вставки для каждого элемента списка НОВЫЙ:

>(defun РАССТАВЬ (новый 1 порядок) (cond

((null новый) 1)

(t (вставь (car новый) (РАССТАВЬ (cdr новый) 1

порядок) порядок))))

РАССТАВЬ

>(расставь '(b c) '(a b d) '(a b с d e)) (А В В С D)

Эту рекурсивную по аргументам функцию можно записать и в виде функции, рекурсивной по значению:

(defun РАССТАВЬ (новый l порядок) (cond

((null новый) 1)

(t (РАССТАВЬ (cdr новый) (вставь (саг новый) 1 порядок) порядок))))

Выразительные возможности рекурсии уже видны из приведенных выше содержательных и занимающих мало места определений. Определения функций В-ОДИН-УРОВЕНЬ и ОБРАЩЕНИЕ в итеративном варианте не поместились бы на один лист бумаги. С помощью рекурсии легко работать с динамическими, заранее не определенными целиком, но достаточно регулярными

91

структурами, такими как списки произвольной длины и глубины вложения.

Используя все более мощные виды рекурсии, можно записывать относительно лаконичными средствами и более сложные вычисления. Одновременно с этим, поскольку определения довольно абстрактны, растет сложность программирования и понимания программы.

Рассмотрим теперь программирование вложенных циклов в такой форме, при которой в определении функции рекурсивный вызов является аргументом вызова этой же самой функции. В такой рекурсии можно выделить различные порядки в соответствии

стем, на каком уровне рекурсии находится вызов. Такую форму рекурсии будем называть рекурсией более высокого порядка. Функции, которые мы до сих пор определяли, были функциями

срекурсией нулевого порядка.

Вкачестве классического примера рекурсии более высокого порядка часто приводится известная из теории рекурсивных функций функция Аккермана, пользующаяся славой «плохой» функции.

Для справки (из википедии): функция Аккермана – простой пример вычислимой функции, которая не является примитивно рекурсивной. Она принимает два неотрицательных целых числа в качестве параметров и возвращает натуральное число, обозначается A (m, n). Эта функция растет очень быстро, например, число A (4, 4) настолько велико, что количество цифр в порядке этого числа многократно превосходит количество атомов в наблюдаемой части вселенной.

Функция Аккермана (табл. 3) определяется рекурсивно для неотрицательных целых чисел m и n следующим образом:

n 1

 

m 0

 

 

m 0,n 0

A(m,n) A(m 1.1)

 

 

1)

m 0,n 0

A(m 1,A(m,n

92

 

 

 

 

 

 

 

 

 

Таблица 3

 

 

Таблица значений функции Аккермана

 

 

 

 

 

 

 

 

 

 

 

n/m

0

1

2

3

4

 

5

 

 

m

0

1

2

3

5

13

 

65533

 

hyper(2, m, 3) – 3

1

2

3

5

13

65533

 

 

 

hyper(2, m, 4) – 3

 

 

 

 

 

 

 

 

 

 

2

3

4

7

29

265536 − 3

 

 

 

hyper(2, m, 5) – 3

 

 

 

 

 

 

 

 

 

 

 

3

4

5

9

61

2265536

3

 

 

 

hyper(2, m, 6) – 3

 

 

 

 

 

 

 

 

 

4

5

6

11

125

22265536

3

А (4, А (5, 3))

hyper(2, m, 7) – 3

5

6

7

13

253

222265536

3

А (4, А (5, 4))

 

hyper(2, m, 8) – 3

n

n + 1

n + 2

2n + 3

2n+3 −3

 

 

 

 

 

hyper(2, m,

 

 

 

 

 

 

 

 

 

 

n + 3) – 3

 

 

 

 

 

 

 

(всего nблоков

 

 

 

 

 

 

 

 

 

22 2

)

 

 

Может показаться неочевидным, что рекурсия всегда заканчивается. Это следует из того, что при рекурсивном вызове или уменьшается значение m, или значение m сохраняется, но уменьшается значение n. Это означает, что каждый раз пара (m, n) уменьшается с точки зрения лексикографического порядка, значит, значение m в итоге достигнет нуля: для одного значения m существует конечное число возможных значений n (так как первый вызов с данным m был произведен с каким-то опре-

93

деленным значением n, а в дальнейшем, если значение m сохраняется, значение n может только уменьшаться), а количество возможных значений m, в свою очередь, тоже конечно. Однако при уменьшении m значение, на которое увеличивается n, неограниченно и обычно очень велико.

>(defun АККЕРМАН (m n) (cond ((= m 0) (+ n 1))

((= n 0) (АККЕРМАН (– m 1) 1)) (t (АККЕРМАН (– m 1)

(АККЕРМАН m (– n 1))))))

АККЕРМАН >(аккерман 2 2) 7 >(аккерман 3 2) 29

Функция Аккермана является функцией с рекурсией первого порядка. Ее вычисление довольно сложно, и время вычисления растет лавинообразно уже при малых значениях аргумента.

В качестве другого примера функции с рекурсией первого порядка приведем функцию В-ОДИН-УРОВЕНЬ, располагающую элементы списка на одном уровне, которую мы ранее определили, используя параллельную рекурсию:

>(defun в-один-уровень (1) (уравнять 1 nil))

В-ОДИН-УРОВЕНЬ

>(defun ВЫРОВНЯТЬ (1 результат) (cond ((null 1) результат)

((atom 1) (cons 1 результат)) (t (ВЫРОВНЯТЬ (car 1)

(ВЫРОВНЯТЬ (cdr 1) результат)))))

ВЫРОВНЯТЬ

94

Вэтом определении работа функции непосредственно сведена к базовым функциям и рекурсии первого порядка, где аргументом рекурсивного вызова является один рекурсивный вызов. В более раннем определении дополнительно к базовым функциям и рекурсии нулевого порядка мы использовали функцию APPEND. Применяя рекурсию более высокого порядка вычисления, можно представить функцию более абстрактно и с помощью более короткого определения, однако представить себе работу такой функции довольно сложно.

Функция ВЫРОВНЯТЬ работает следующим образом. Результат строится в списке РЕЗУЛЬТАТ. Если L – атом, то его можно непосредственно добавить в начало списка РЕЗУЛЬТАТ. Если L – список и его первый элемент является атомом, то все сводится к предыдущему состоянию на следующем уровне рекурсии, но в такой ситуации, когда список РЕЗУЛЬТАТ содержит уже вытянутый в один уровень оставшийся хвост.

Втом случае, когда и голова списка L является списком, его сначала приводят к одному уровню. Это делается с помощью рекурсивных вызовов, погружающихся в головную ветвь до тех пор, пока там не встретится атом, который можно добавить в начало вытянутой к этому моменту в один уровень структуры. Встречающиеся таким образом атомы добавляются по одному к вытянутому хвосту. На каждом уровне при исчерпании списка на предыдущий уровень выдается набранный к данному моменту РЕЗУЛЬТАТ.

Следующее определение функции REVERSE, использующей лишь базовые функции и рекурсию, является примером еще более глубокого уровня рекурсии:

(defun REV (l) (cond

((null l) l) ((null (cdr 1)) l) (t (cons

(car (REV (cdr l)))

95

(REV (cons (car l)

(REV (cdr (REV (cdr l)

))))))

В определении использована рекурсия второго порядка. Вычисления, представленные этим определением, понять труднее, чем прежние. Сложная рекурсия усложняет и вычисления. В этом случае невозможно вновь воспользоваться полученными ранее результатами, поэтому одни и те же результаты приходится вычислять снова и снова. Обращение списка из пяти элементов функцией REV требует 149 вызовов. Десятиэлементный список требует уже 74 409 вызовов и заметного времени для вычисления. Как правило, многократных вычислений можно избежать, разбив определение на несколько частей и используя подходящие параметры для сохранения и передачи промежуточных результатов.

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

Если коротко, то все сказанное о рекурсии можно выразить следующим образом. Рекурсия по значению – это когда вызов является выражением, определяющим результат функции. Если же в качестве результата функции возвращается значение некоторой другой функции и рекурсивный вызов участвует в вычислении аргументов этой функции, то это рекурсия по аргументам. Аргументом рекурсивного вызова может быть вновь рекурсивный вызов, и таких вызовов может быть много.

Простая рекурсия – это когда вызов функции встречается в некоторой ветви лишь один раз.

Наблюдать за работой функции в динамике можно при помощи содержащихся в интерпретаторе средств трассировки. Трассировка функции включается при помощи директивы, которая в отличие от обычных функций не вычисляет свой аргумент.

96

Трассировку можно отменить аналогичной по форме дирек-

тивой UNTRACE.

MEMBER – проверяет, принадлежит ли элемент списку. REMOVE – удаляет элемент из списка.

SUBSTITUTE – заменяет все вхождения элемента.

2.11. Функциональные аргументы

До сих пор мы рассматривали функции, которые в качестве аргументов получали формы, представляющие собой выражения, являющиеся данными. Значения функций также считались относящимися по типу к данным, т.е. выражениям, трактуемым как данные.

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

Аргумент, значением которого является функция, называют в функциональном программировании функциональным аргументом, а функцию, имеющую функциональный аргумент, – функционалом. Иными словами, функция от функции.

Различие между понятиями «данные» и «функция» определяется не на основе их структуры, а в зависимости от их использования. Если аргумент используется в функции лишь как объект, участвующий в вычислениях, то мы имеем дело с обыкновенным аргументом, представляющим данные. Если же он используется как средство, определяющее вычисления, т.е. играет в вычислениях, например, роль лямбда-выражения, которое применяется к другим аргументам, то мы имеем дело с функцией.

Одно и то же выражение может в связи с различными аспектами выступать, с одной стороны, как обыкновенный аргумент, а с другой стороны, как функциональный. Роль и интерпретация выражения зависят от его синтаксической позиции.

97

Пример:

 

>(car '(lambda (x)

 

(list x)))

; CAR – функция

LAMBDA

; лямбда-выражение – данные

>((lambda (x)

(list х)) 'саr) ; CAR – данные

(CAR) ; лямбда-выражение – функция Переданное функции CAR лямбда-выражение не является

функциональным аргументом, и CAR не становится функционалом. То же самое – лямбда-выражение, стоящее в позиции функции, уже интерпретируется как функция, в то время как CAR интерпретируется как данные. Функциональный аргумент и функционал являются некоторым обобщением простого понятия функции: функциональным аргументом может быть любой подходящий объект, который используется в теле функционала в позиции функции и в роли функции.

Далее мы будем использовать понятия функции, вызова функции и значения функции в следующем смысле:

1)Функция сама есть изображение вычислений или опре-

деление.

2)Вызов функции есть применение этого изображения.

3)Значение функции есть результат такого применения. Функция является функционалом, если в качестве ее ар-

гумента используется лисповский объект типа (1), который интерпретируется как вторая функция в теле функционала. Таким функциональным объектом может быть:

1)символьное имя, представляющее определение функции (системная функция или функция, определенная пользователем),

2)безымянное лямбда-выражение,

3)замыкание.

Фактический параметр для функционального аргумента функционала задается в виде формы, значением которой будет объект, который можно интерпретировать как функцию.

98

Примеры: '(lambda (x) (list x))

(list 'lambda '(x) (list 'list 'x)) (first '(car cdr cons))

2.12. Функциональное значение функции

Аргументом функции может быть функция, однако функция может быть и результатом. Такие функции называют функциями с функциональным значением. Функционал также может быть с функциональным значением. Вызов такого функционала возвращает в качестве результата новую функцию, в построении которой, возможно, используются функции, получаемые функционалом в качестве аргументов. Такие функционалы будем называть функционалами с функциональным значением.

Вызов функции с функциональным значением или другая форма с функциональным значением может в вызове функции находиться в двух различных позициях:

1)функционального аргумента в вызове функционала;

2)на месте имени функции в вызове функции (или функционала, или функции с функциональным значением).

В «Коммон Лиспе» предполагается, что в вызове функции на месте имени функции находится символ, определенный

спомощью формы DEFUN как имя функции. Переданный в качестве параметра функциональный объект можно использовать лишь через явный вызов применяющих функционалов (FUNCALL, APPLY). В некоторых других системах такого ограничения нет, и в вызове функции на месте имени функции может быть любая форма, имеющая функциональное значение. Когда именем в вызове является атом без функциональной связи, то в качестве функционального объекта берется значение этого имени (SYMBOL-VALUE).

99

Способы композиций функций

Все типы функций, которые до сих пор встречались, могут быть использованы в определениях (лямбда-выражениях) в следующих позициях:

1. Обыкновенный вызов:

(defun f … … (g …) …).

2. Рекурсивный вызов:

(defun f … … (f …) …).

3.Вложенный рекурсивный вызов: (defun f … … (f … (f …) …) …).

4.Функциональный аргумент:

(defun f (… g…) … (apply g …) …).

5. Рекурсивный функциональный аргумент:

(defun f (… f…) … (apply f… f …) …).

Аргументами функций были данные, выражения, значением которых являются данные или, как в случае функций более высокого порядка, другие функции.

Объединяя использование рекурсии и функционалов, остановимся на еще одном способе использования, когда функция принимает саму себя в качестве функционального аргумента.

Получающий себя в качестве аргумента функционал называют применяемым к самому себе, или автоаппликативной функцией. Соответственно функцию, возвращающую саму себя, называют авторепликативной. Часто говорят и о самовоспроизводящихся функциях.

Автоаппликативные и авторепликативные функции образуют класс автофункций. Автофункции на самом деле не получают в качестве параметра и не возвращают в качестве результата буквально самих себя, а лишь используют или копируют себя.

100