- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
206 |
Глава 12. Структура |
> whole (A B E)
Разумеется, то же самое будет происходить и для двух списков, имею щих общий хвост.
Изменение двух объектов одновременно не всегда является ошибкой. Иногда это именно то, что нужно. Однако если такое изменение проис ходит непреднамеренно, оно может привести к некорректной работе программы. Опытные программисты умеют избегать подобных ошибок и немедленно распознавать такие ситуации. Если список без видимой причины меняет свое содержимое, вероятно, он имеет разделяемую структуру.
Опасна не сама разделяемая структура, а возможность ее изменения. Чтобы гарантировать отсутствие подобных ошибок, просто избегайте использования setf (а также аналогичных операторов типа pop, rplaca и других) для списков. Если изменяемость списков все же требуется, то необходимо выяснить, откуда взялся изменяемый список, чтобы убе диться, что он не делит структуру с чем-то, что нельзя менять. Если же это не так или вам неизвестно происхождение списка, то модифициро вать необходимо не сам список, а его копию.
Нужно быть вдвойне осторожным при использовании функций, напи санных кем-то другим. Пока не установлено обратное, имейте в виду, что все, что передается функции:
1.Может быть передано деструктивным операторам.
2.Может быть сохранено где-либо, и изменение этого объекта приведет к изменению в других частях кода, использующего данный объект.1
Вобоих случаях правильным решением является копирование аргу ментов.
ВCommon Lisp вызов любой функции, выполняющейся во время про хода по структуре (например, функции-аргумента mapcar или remove-if), не должен менять эту структуру. В противном случае последствия вы полнения такого кода не определены.
12.3.Пример: очереди
Разделяемые структуры – это не только повод для беспокойства. Ино гда они могут быть полезны. В этом разделе показано, как с помощью разделяемых структур представить очереди. Очередь – это хранилище объектов, из которого они могут быть извлечены по одному в том же
1Например, в Common Lisp попытка изменения строки, используемой как имя символа, считается ошибкой. И поскольку нам неизвестно, копирует ли intern свой аргумент-строку перед созданием символа, мы должны пред положить, что модификация любой строки, которая передавалась в intern, приведет к ошибке.
12.3. Пример: очереди |
207 |
порядке, в котором они были туда записаны. Такую модель принято именовать FIFO – сокращение от «первым пришел, первым ушел» («first in, first out»).
С помощью списков легко представить стопку, так как добавление и по лучение элементов происходит с одного конца. Задача представления очереди более сложна, поскольку добавление и извлечение объектов про исходит с разных концов. Для эффективной ее реализации необходимо каким-то образом обеспечить управление обоими концами списка.
Одна из возможных стратегий приводится на рис. 12.6, где показана очередь из трех элементов: a, b и c. Очередью считаем точечную пару, состоящую из списка и последней ячейки этого же списка. Будем назы вать их начало и конец. Чтобы получить элемент из очереди, необходи мо просто извлечь начало. Чтобы добавить новый элемент, необходимо создать новую ячейку, сделать ее cdr конца очереди и затем сделать ее же концом.
q1 =
nil
a |
b |
с |
Рис. 12.6. Структура очереди
Такую стратегию реализует код на рис. 12.7.
(defun make-queue () (cons nil nil))
(defun enqueue (obj q) (if (null (car q))
(setf (cdr q) (setf (car q) (list obj))) (setf (cdr (cdr q)) (list obj)
(cdr q) (cdr (cdr q))))
(car q))
(defun dequeue (q) (pop (car q)))
Рис. 12.7. Реализация очередей
Она используется следующим образом:
> (setf q1 (make-queue)) (NIL)
208 |
Глава 12. Структура |
> (progn (enqueue ’a q1) (enqueue ’b q1) (enqueue ’c q1))
(A B C)
Теперь q1 представляет очередь, изображенную на рис. 12.6:
> q1
((A B C) C)
Попробуем забрать из очереди несколько элементов:
>(dequeue q1)
A
>(dequeue q1)
B
>(enqueue ’d q1) (C D)
12.4.Деструктивные функции
Common Lisp включает в себя несколько функций, которые могут изме нять структуру списков и за счет этого работать быстрее. Они называ ются деструктивными. И хотя эти функции могут изменять ячейки, переданные им в качестве аргументов, они делают это не ради побоч ных эффектов.
Например, delete является деструктивным аналогом remove. Хотя ей и позволено портить переданный список, она не дает никаких обеща ний, что так и будет делать. Посмотрим, что происходит в большинстве реализаций:
>(setf lst ’(a r a b i a)) (A R A B I A)
>(delete ’a lst)
(R B I) > lst
(A R B I)
Как и в случае с remove, чтобы зафиксировать побочный эффект, необхо димо использовать setf:
(setf lst (delete ’a lst))
Примером того, как деструктивные функции модифицируют списки, яв ляется nconc, деструктивная версия append.1 Приведем ее версию для двух аргументов, демонстрирующую, каким образом сшиваются списки:
(defun nconc2 (x y) (if (consp x)
1Буква n в имени nconc соответствует «non-consing». Имена многих деструк тивных функций начинаются с n.
12.5. Пример: двоичные деревья поиска |
209 |
(progn
(setf (cdr (last x)) y) x)
y))
cdr последней ячейки первого списка становится указателем на второй список. Полноценная версия для произвольного числа аргументов при водится в приложении B.
Функция mapcan похожа на mapcar, но соединяет в один список возвра щаемые значения (которые должны быть списками) с помощью nconc:
> (mapcan #’list ’(a b c)
’(1 2 3 4)) (A 1 B 2 C 3)
Эта функция может быть определена следующим образом:
(defun our-mapcan (fn &rest lsts)
(apply #’nconc (apply #’mapcar fn lsts)))
Используйте mapcan с осторожностью, учитывая ее деструктивный ха рактер. Она соединяет возвращаемые списки с помощью nconc, поэтому их лучше больше нигде не задействовать.
Функция mapcan полезна, в частности, в задачах, интерпретируемых как сбор всех узлов одного уровня некоего дерева. Например, если children возвращает список чьих-то детей, тогда мы сможем определить функ цию для получения списка внуков так:
(defun grandchildren (x) (mapcan #’(lambda (c)
(copy-list (children c))) (children x)))
Данная функция применяет copy-list к результату вызова children, так как он может возвращать уже существующий объект, а не производить новый.
Также можно определить недеструктивный вариант mapcan:
(defun mappend (fn &rest lsts)
(apply #’append (apply #’mapcar fn lsts)))
Используя mappend, мы можем обойтись без вызовов copy-list в опреде лении grandchildren:
(defun grandchildren (x)
(mappend #’children (children x)))
12.5.Пример: двоичные деревья поиска
Внекоторых ситуациях уместнее использовать деструктивные опера ции, нежели недеструктивные. В разделе 4.7 было показано, как управ
210 |
Глава 12. Структура |
лять двоичными деревьями поиска (BST). Все использованные там функ ции были недеструктивными, но если нужно применить BST на практи ке, такая осторожность излишня.
На рис. 12.8 приведена деструктивная версия bst-insert (стр. 86). Она принимает точно такие же аргументы и возвращает точно такое же зна чение, как и исходная версия. Единственным отличием является то, что она может изменять дерево, передаваемое вторым аргументом.
(defun bst-insert! (obj bst <) (if (null bst)
(make-node :elt obj) (progn (bsti obj bst <)
bst)))
(defun bsti (obj bst <)
(let ((elt (node-elt bst))) (if (eql obj elt)
bst
(if (funcall < obj elt) (let ((l (node–l bst)))
(if l
(bsti obj l <) (setf (node–l bst)
(make-node :elt obj)))) (let ((r (node-r bst)))
(if r
(bsti obj r <) (setf (node-r bst)
(make-node :elt obj))))))))
Рис. 12.8. Двоичные деревья поиска: деструктивная вставка
В разделе 2.12 было предупреждение о том, что деструктивные функ ции вызываются не ради побочных эффектов. Поэтому если вы хотите построить дерево с помощью bst-insert!, вам нужно вызывать ее так же, как если бы вы вызывали настоящую bst-insert:
>(setf *bst* nil) NIL
>(dolist (x ’(7 2 9 8 4 1 5 12))
(setf *bst* (bst-insert! x *bst* #’<)))
NIL
Вы могли бы также определить аналог push для BST, но это довольно сложно и выходит за рамки данной книги. (Для любопытных такой макрос определяется на стр. 429.°)
12.5. Пример: двоичные деревья поиска |
211 |
На рис. 12.91 представлен деструктивный вариант функции bst-delete, которая связана с bst-remove (стр. 89) так же, как delete связана с remove. Как и delete, она не подразумевает вызов ради побочных эффектов. Ис пользовать bst-delete необходимо так же, как и bst-remove:
>(setf *bst* (bst-delete 2 *bst* #’<)) #<7>
>(bst-find 2 *bst* #’<)
NIL
(defun bst-delete (obj bst <) (if (null bst)
nil
(if (eql obj (node-elt bst)) (del-root bst)
(progn
(if (funcall < obj (node-elt bst))
(setf (node-l bst) (bst-delete obj (node-l bst) <)) (setf (node-r bst) (bst-delete obj (node-r bst) <)))
bst))))
(defun del-root (bst)
(let ((l (node-l bst)) (r (node-r bst)))
(cond ((null l) r) |
|
((null r) l) |
|
(t |
(if (zerop (random 2)) |
|
(cutnext r bst nil) |
(cutprev l bst nil))))))
(defun cutnext (bst root prev) (if (node-l bst)
(cutnext (node-l bst) root bst)
(if prev |
|
(progn |
|
(setf (node-elt root) |
(node-elt bst) |
(node-l prev) |
(node-r bst)) |
root) |
|
(progn |
|
(setf (node-l bst) |
(node-l root)) |
bst)))) |
|
(defun cutprev (bst root prev) (if (node-r bst)
(cutprev (node-r bst) root bst) (if prev
(progn
(setf (node-elt root) (node-elt bst)
1Данный листинг содержит исправленную версию bst-delete. За подробно стями обращайтесь к сноске на стр. 89. – Прим. перев.