- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 1. Введение
- •1.1. Новые инструменты
- •1.2. Новые приемы
- •1.3. Новый подход
- •Глава 2. Добро пожаловать в Лисп
- •2.1. Форма
- •2.2. Вычисление
- •2.3. Данные
- •2.4. Операции со списками
- •2.5. Истинность
- •2.6. Функции
- •2.7. Рекурсия
- •2.8. Чтение Лиспа
- •2.9. Ввод и вывод
- •2.10. Переменные
- •2.11. Присваивание
- •2.12. Функциональное программирование
- •2.13. Итерация
- •2.14. Функции как объекты
- •2.15. Типы
- •2.16. Заглядывая вперед
- •Итоги главы
- •Упражнения
- •Глава 3. Списки
- •3.1. Ячейки
- •3.2. Равенство
- •3.3. Почему в Лиспе нет указателей
- •3.4. Построение списков
- •3.5. Пример: сжатие
- •3.6. Доступ
- •3.7. Отображающие функции
- •3.8. Деревья
- •3.9. Чтобы понять рекурсию, нужно понять рекурсию
- •3.10. Множества
- •3.11. Последовательности
- •3.12. Стопка
- •3.13. Точечные пары
- •3.14. Ассоциативные списки
- •3.15. Пример: поиск кратчайшего пути
- •3.16. Мусор
- •Итоги главы
- •Упражнения
- •Глава 4. Специализированные структуры данных
- •4.1. Массивы
- •4.2. Пример: бинарный поиск
- •4.3. Строки и знаки
- •4.4. Последовательности
- •4.5. Пример: разбор дат
- •4.6. Структуры
- •4.7. Пример: двоичные деревья поиска
- •4.8. Хеш-таблицы
- •Итоги главы
- •Упражнения
- •Глава 5. Управление
- •5.1. Блоки
- •5.2. Контекст
- •5.3. Условные выражения
- •5.4. Итерации
- •5.5. Множественные значения
- •5.6. Прерывание выполнения
- •5.7. Пример: арифметика над датами
- •Итоги главы
- •Упражнения
- •Глава 6. Функции
- •6.1. Глобальные функции
- •6.2. Локальные функции
- •6.3. Списки параметров
- •6.4. Пример: утилиты
- •6.5. Замыкания
- •6.6. Пример: строители функций
- •6.7. Динамический диапазон
- •6.8. Компиляция
- •6.9. Использование рекурсии
- •Итоги главы
- •Упражнения
- •Глава 7. Ввод и вывод
- •7.1. Потоки
- •7.2. Ввод
- •7.3. Вывод
- •7.4. Пример: замена строк
- •7.5. Макрознаки
- •Итоги главы
- •Упражнения
- •Глава 8. Символы
- •8.1. Имена символов
- •8.2. Списки свойств
- •8.3. А символы-то не маленькие
- •8.4. Создание символов
- •8.5. Использование нескольких пакетов
- •8.6. Ключевые слова
- •8.7. Символы и переменные
- •8.8. Пример: генерация случайного текста
- •Итоги главы
- •Упражнения
- •Глава 9. Числа
- •9.1. Типы
- •9.2. Преобразование и извлечение
- •9.3. Сравнение
- •9.4. Арифметика
- •9.5. Возведение в степень
- •9.6. Тригонометрические функции
- •9.7. Представление
- •9.8. Пример: трассировка лучей
- •Итоги главы
- •Упражнения
- •Глава 10. Макросы
- •10.1. Eval
- •10.2. Макросы
- •10.3. Обратная кавычка
- •10.4. Пример: быстрая сортировка
- •10.5. Проектирование макросов
- •10.6. Обобщенные ссылки
- •10.7. Пример: макросы-утилиты
- •10.8. На Лиспе
- •Итоги главы
- •Упражнения
- •Глава 11. CLOS
- •11.1. Объектно-ориентированное программирование
- •11.2. Классы и экземпляры
- •11.3. Свойства слотов
- •11.4. Суперклассы
- •11.5. Предшествование
- •11.6. Обобщенные функции
- •11.7. Вспомогательные методы
- •11.8. Комбинация методов
- •11.9. Инкапсуляция
- •11.10. Две модели
- •Итоги главы
- •Упражнения
- •Глава 12. Структура
- •12.1. Разделяемая структура
- •12.2. Модификация
- •12.3. Пример: очереди
- •12.4. Деструктивные функции
- •12.5. Пример: двоичные деревья поиска
- •12.6. Пример: двусвязные списки
- •12.7. Циклическая структура
- •12.8. Неизменяемая структура
- •Итоги главы
- •Упражнения
- •Глава 13. Скорость
- •13.1. Правило бутылочного горлышка
- •13.2. Компиляция
- •13.3. Декларации типов
- •13.4. Обходимся без мусора
- •13.5. Пример: заранее выделенные наборы
- •13.6. Быстрые операторы
- •13.7. Две фазы разработки
- •Итоги главы
- •Упражнения
- •Глава 14. Более сложные вопросы
- •14.1. Спецификаторы типов
- •14.2. Бинарные потоки
- •14.3. Макросы чтения
- •14.4. Пакеты
- •14.5. Loop
- •14.6. Особые условия
- •Глава 15. Пример: логический вывод
- •15.1. Цель
- •15.2. Сопоставление
- •15.3. Отвечая на запросы
- •15.4. Анализ
- •Глава 16. Пример: генерация HTML
- •16.1. HTML
- •16.2. Утилиты HTML
- •16.3. Утилита для итерации
- •16.4. Генерация страниц
- •Глава 17. Пример: объекты
- •17.1. Наследование
- •17.2. Множественное наследование
- •17.3. Определение объектов
- •17.4. Функциональный синтаксис
- •17.5. Определение методов
- •17.6. Экземпляры
- •17.7. Новая реализация
- •17.8. Анализ
- •Комментарии
- •Алфавитный указатель
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 поддерживают такую оптимизацию. – Прим. перев.