Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Pol_Grem_-_ANSI_Common_Lisp_High_tech_-_2012.pdf
Скачиваний:
28
Добавлен:
12.03.2016
Размер:
4.85 Mб
Скачать

124

Глава 6. Функции

(foo))

20

Форма­ declare может­ начи­нать­ любое­ тело­ кода,­ в кото­ром­ созда­ют­ся­ новые­ пере­мен­ные­. Декла­ра­ция­ special уникаль­на­ тем, что может­ изме­­ нить пове­де­ние­ програм­мы­. В главе 13 будут­ рассмот­ре­ны­ другие­ дек­ лара­ции­. Прочие­ из них явля­ют­ся­ всего­ лишь сове­та­ми­ компи­ля­то­ру;­ они могут­ сделать­ програм­му­ быст­рее,­ но не меня­ют­ ее пове­де­ние­.

Глобаль­ные­ пере­мен­ные,­ уста­нов­лен­­ные с помо­щью­ setf в toplevel, под­ разу­ме­ва­ют­ся­ специ­аль­ны­ми:­

>(setf x 30)

30

>(foo)

30

Но в исход­ном­ коде­ лучше­ не пола­гать­ся­ на такие­ неяв­ные­ опре­де­ле­ния­ и исполь­зо­вать­ defparameter.

В каких­ случа­ях­ может­ быть полезен­ ди­нами­че­ский­ диапа­зон?­ Обычно­ он приме­ня­ет­ся,­ чтобы­ присво­ить­ неко­то­рой­ глобаль­ной­ пере­мен­ной­ новое­ времен­ное­ значе­ние­. Напри­мер,­ для управле­ния­ пара­мет­ра­ми­ печа­ти­ объек­тов­ исполь­зу­ют­ся­ 11 глобаль­ных­ пере­мен­ных,­ включая­ *print-base*, кото­рая­ по умолча­нию­ уста­нов­ле­на­ как 10. Чтобы­ печа­тать­ числа­ в шест­на­дца­те­рич­ном­ виде­ (с осно­ва­ни­ем­ 16), мож­но привя­зать­ *print-base* к соот­вет­ст­вую­щей­ базе:­

> (let ((*print-base* 16)) (princ 32))

20

32

Здесь отобра­же­ны­ два числа:­ вывод­ princ и значе­ние,­ кото­рое­ она воз­ враща­ет­. Они представ­ля­ют­ собой­ одно­ и то же значе­ние,­ напе­ча­тан­ное­ снача­ла­ в шест­на­дца­те­рич­ном­ форма­те,­ так как *print-base* имела­ зна­ чение­ 16, а затем­ в деся­тич­ном,­ посколь­ку­ на возвра­щае­мое­ из let значе­­ ние уже не дейст­во­ва­ло­ *print-base* 16.

6.8.Компиляция

ВCommon Lisp можно­ компи­ли­ро­вать­ функции­ по отдель­но­сти­ или же файл цели­ком­. Если­ вы просто­ набе­ре­те­ опре­де­ле­ние­ функции­ в toplevel:­

> (defun foo (x) (+ x 1)) FOO

то многие­ реали­за­ции­ созда­дут­ интер­пре­ти­руе­мый­ код. Прове­рить,­ яв­ ляет­ся­ ли функция­ скомпи­ли­ро­ван­ной,­ мож­но с помо­щью­ compiled- function-p:

> (compiled-function-p #’foo) NIL

6.9. Использование рекурсии

125

Функцию­ можно­ скомпи­ли­ро­вать,­ сооб­щив­ compile имя функции:­

> (compile ’foo) FOO

После­ этого­ интер­пре­ти­ро­ван­ное­ опре­де­ле­ние­ функции­ будет­ заме­не­но­ скомпи­ли­ро­ван­ным­. Скомпи­ли­ро­ван­ные­ и интер­пре­ти­ро­ван­ные­ функ­ ции ведут­ себя­ абсо­лют­но­ одина­ко­во,­ за исклю­че­ни­ем­ отно­ше­ния­ к com­ piled-function-p.

Функция­ compile также­ прини­ма­ет­ списки­. Такое­ исполь­зо­ва­ние­ compi­ le обсу­ж­да­ет­ся­ на стр. 171.

К неко­то­рым­ функци­ям­ compile не­приме­ни­ма:­ это функции­ типа­ stamp или reset, опре­де­лен­­ные через­ toplevel в собст­вен­­ном (создан­ном­ let) лекси­че­ском­ контек­сте­1. Вам придет­ся­ набрать­ эти функции­ в файле,­ затем­ его ском­пили­ро­вать­ и загру­зить­. Этот запрет­ уста­нов­лен­ по тех­ ниче­ским­ причи­нам,­ а не пото­му,­ что что-то не так с опре­де­ле­ни­ем­ функций­ в иных лекси­че­ских­ окру­же­ни­ях­.

Чаще­ всего­ функции­ компи­ли­ру­ют­ся­ не по отдель­но­сти,­ а в соста­ве­ файла­ с помо­щью­ compile-file. Эта функция­ созда­ет­ скомпи­ли­ро­ван­­ ную версию­ задан­но­го­ файла,­ как прави­ло,­ с тем же именем,­ но другим­ расши­ре­ни­ем­. После­ загруз­ки­ скомпи­ли­ро­ван­но­го­ файла­ compiled-func­ tion-p вернет­ исти­ну­ для любой­ функции­ из этого­ файла­.

Если­ одна­ функция­ исполь­зу­ет­ся­ внутри­ другой,­ то она так­же долж­на бытьскомпи­ли­ро­ва­на­.Таким­обра­зом,­make-adder (стр.119),буду­чи­ском­ пили­ро­ван­ной,­ возвра­ща­ет­ скомпи­ли­ро­ван­ную­ функцию:­

>(compile ’make-adder) MAKE-ADDER

>(compiled-funcion-p (make-adder 2))

T

6.9.Использование рекурсии

ВЛиспе­ рекур­сия­ имеет­ большее­ значе­ние,­ чем в других­ язы­ках. Этому­ есть три основ­ные­ причи­ны:­

1.Функцио­наль­ное­ програм­ми­ро­ва­ние­. Рекур­сив­ные­ алго­рит­мы­ в мень­ шей мере­ нуж­да­ют­ся­ в исполь­зо­ва­нии­ побоч­ных­ эффек­тов­.

2.Рекур­сив­ные­ структу­ры­ данных­. Неяв­ное­ исполь­зо­ва­ние­ указа­те­­ лей в Лиспе­ облег­ча­ет­ рекур­сив­ное­ созда­ние­ структур­ данных­. Наи­ более­ общей­ структу­рой­ тако­го­ типа­ явля­ет­ся­ список:­ либо­ nil, либо­ cons, чей cdr – также­ список­.

3.Элегант­ность­. Лисп-програм­ми­сты­ прида­ют­ огром­ное­ значе­ние­ кра­ соте­ их программ,­ а рекур­сив­ные­ алго­рит­мы­ зачас­тую­ выгля­дят­ на­ много­ элегант­нее­ их итера­тив­ных­ анало­гов­.

1В Лиспах,­ суще­ст­во­вав­ших­ до ANSI Common Lisp, первый­ аргу­мент­ compile не мог быть уже скомпи­ли­ро­ван­ной­ функци­ей­.

126

Глава 6. Функции

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

Это же утвер­жде­ние­ верно­ и в том случае,­ когда­ вам необ­хо­ди­мо­ напи­­ сать свою рекур­сив­ную­ функцию­. Если­ вы сможе­те­ сформу­ли­ро­вать­ ре­ курсив­ное­ реше­ние­ пробле­мы,­ то вам не соста­вит­ труда­ пере­вес­ти­ это реше­ние­ в код. Чтобы­ решить­ зада­чу­ с помо­щью­ рекур­сии,­ необ­хо­ди­мо­ сделать­ следую­щее:­

1.Нуж­но пока­зать,­ как можно­ решить­ ее с помо­щью­ разде­ле­ния­ на ко­ нечное­ число­ похо­жих,­ но более­ мелких­ подза­дач­.

2.Нуж­но пока­зать,­ как решить­ самую­ малень­кую­ подза­да­чу­ (базо­вый­ случай)­ с помо­щью­ конеч­но­го­ набо­ра­ опера­ций­.

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

Напри­мер,­ в предло­жен­ном­ ниже­ рекур­сив­ном­ алго­рит­ме­ нахо­ж­де­ния­ длины­ списка­ мы на каждом­ шаге­ рекур­сии­ нахо­дим­ длину­ уменьшен­­ ного­ списка:­

1.В общем­ случае­ длина­ списка­ равна­ длине­ его cdr, увели­чен­­ной на 1.

2.Длину­ пусто­го­ списка­ прини­ма­ем­ равной­ 0.

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

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

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

member Объект­ содер­жит­ся­ в списке,­ если­ он явля­ет­ся­ его первым­ элемен­том­ или содер­жит­ся­ в cdr этого­ списка­. В пустом­ спи­ ске не содер­жит­ся­ ниче­го­.

copy-tree Копия­ дере­ва,­ представ­лен­­ного­ как cons-ячей­ка, – это ячей­ ка, постро­ен­ная­ из copy-tree для car исход­ной­ ячей­ки и copytree для ее cdr. Для атома­ copy-tree – это сам этот атом.

6.9. Использование рекурсии

127

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

Неко­то­рые­ алго­рит­мы­ есте­ст­вен­­ным обра­зом­ ложат­ся­ на такие­ опре­де­­ ления,­ но не все. Вам придет­ся­ согнуть­ся­ в три поги­бе­ли,­ чтобы­ опре­де­­ лить our-copy-tree (стр. 205) без исполь­зо­ва­ния­ рекур­сии­. С другой­ сто­ роны, итера­тив­ный­ вари­ант­ show-squares (стр. 40) более­ досту­пен­ для понима­ния,­ неже­ли­ его рекур­сив­ный­ аналог­ (стр. 41). Часто­ опти­маль­­ ный выбор­ оста­ет­ся­ неясен­ до тех пор, пока­ вы не присту­пи­те­ к напи­са­­ нию кода­.

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

Кроме­ того,­ вам необ­хо­ди­мо­ помнить,­ что рекур­сив­ный­ по сути­ алго­­ ритм не всегда­ эффек­ти­вен­ сам по себе­. Класси­че­ский­ пример ­– функ­ ция Фибо­нач­чи­. Эта функция­ рекур­сив­на­ по опре­де­ле­нию:­

1.Fib(0) = Fib(1) = 1.

2.Fib(n) = Fib(n – 1) + Fib(n – 2).

При этом дослов­ная­ трансля­ция­ данно­го­ опре­де­ле­ния­ в код:

(defun fib (n) (if (<= n 1)

1

(+ (fib (- n 1)) (fib (- n 2)))))

дает­ совер­шен­но­ неэф­фек­тив­ную­ функцию­. Дело­ в том, что такая­ функ­ ция вычис­ля­ет­ одни­ и те же вещи­ по несколь­ко­ раз. Напри­мер,­ вычис­­ ляя (fib 10), она вызо­вет­ (fib 9) и (fib 8). Одна­ко­ вычис­ле­ние­ (fib 9) уже включа­ет­ в себя­ (fib 8), и полу­ча­ет­ся,­ что она выпол­ня­ет­ это вычис­ле­­ ние зано­во­.

Ниже­ приве­де­на­ анало­гич­ная­ итера­тив­ная­ функция:­

(defun fib (n)

(do ((i n (- i 1)) (f1 1 (+ f1 f2)) (f2 1 f1))

((<= i 1) f1)))

1В дейст­ви­тель­но­сти,­ хвосто­вая­ рекур­сия­ просто­ преоб­ра­зу­ет­ся­ в соот­вет­ст­­ вующий­ цикл. Такая­ опти­ми­за­ция­ хвосто­вой­ рекур­сии­ входит­ в стандарт­ языка­ Scheme, но отсут­ст­ву­ет­ в Common Lisp. Тем не менее­ многие­ компи­­ лято­ры­ Common Lisp поддер­жи­ва­ют­ такую­ опти­ми­за­цию­. – Прим. перев­.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]