- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
3.13. Точечные пары |
65 |
3.13. Точечные пары
Списки, которые могут быть построены с помощью list, называются правильными списками (proper list). Правильным списком считается либо nil, либо cons-ячейка, cdr которой – также правильный список. Та ким образом, можно определить предикат, который возвращает истину только для правильного списка1:
(defun proper-list? (x) (or (null x)
(and (consp x)
(proper-list? (cdr x)))))
Оказывается, с помощью cons можно создавать не только правильные списки, но и структуры, содержащие ровно два элемента. При этом car соответствует первому элементу структуры, а cdr – второму.
> (setf pair (cons ’a ’b)) (A . B)
Поскольку эта cons-ячейка не является правильным списком, при ото бражении ее car и cdr разделяются точкой. Такие ячейки называются
точечными парами (рис. 3.10).
a b
Рис. 3.10. Ячейка как точечная пара
Правильные списки можно задавать и в виде набора точечных пар, но они будут отображаться в виде списков:
> ’(a . (b . (c . nil))) (A B C)
Обратите внимание, как соотносятся ячеечная и точечная нотации – рис. 3.2 и 3.10.
Допустима также смешанная форма записи:
> (cons ’a (cons ’b (cons ’c ’d))) (A B C . D)
Структура такого списка показана на рис. 3.11.
1Вообще говоря, это определение слегка некорректно, т. к. не всегда будет возвращать nil для любого объекта, не являющегося правильным списком. Например, для циклического списка она будет работать бесконечно. Цик лические списки обсуждаются в разделе 12.7.
66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Глава 3. Списки |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
a |
b |
c d |
Рис. 3.11. Список точечных пар
Таким образом, список (a b) может быть записан аж четырьмя спосо бами:
(a . (b . nil)) (a . (b))
(a b . nil) (a b)
ипри этом Лисп отобразит их одинаково.
3.14.Ассоциативные списки
Также вполне естественно задействовать cons-ячейки для представле ния отображений. Список точечных пар называется ассоциативным списком (assoc-list, alist). С помощью него легко определить набор каких- либо правил и соответствий, к примеру:
> (setf trans ’((+ . "add") (- . "subtract"))) ((+ . "add") (- . "subtract"))
Ассоциативные списки медленные, но они удобны на начальных этапах работы над программой. В Common Lisp есть встроенная функция assoc для получения по ключу соответствующей ему пары в таком списке:
>(assoc ’+ trans) (+ . "add")
>(assoc ’* trans) NIL
Если assoc ничего не находит, возвращается nil.
Попробуем определить упрощенный вариант функции assoc:
(defun our-assoc (key alist) (and (consp alist)
(let ((pair (car alist))) (if (eql key (car pair))
pair
(our-assoc key (cdr alist))))))
Как и member, реальная функция assoc принимает несколько аргументов по ключу, включая :test и :key. Также Common Lisp определяет assoc-if, которая работает по аналогии с member-if.
3.15. Пример: поиск кратчайшего пути |
67 |
3.15. Пример: поиск кратчайшего пути
На рис. 3.12 показана программа, вычисляющая кратчайший путь на графе (или сети) . Функции shortest-path необходимо сообщить началь ную и конечную точки, а также саму сеть, и она вернет кратчайший путь между ними, если он вообще существует.
В этом примере узлам соответствуют символы, а сама сеть представле на как ассоциативный список элементов вида (узел . соседи).
Небольшая сеть, показанная на рис. 3.13, может быть записана так:
(setf min ’((a b c) (b c) (c d)))
Найти узлы, в которые можно попасть из узла a, поможет функция assoc:
> (cdr (assoc ’a min)) (B C)
(defun shortest-path (start end net) (bfs end (list (list start)) net))
(defun bfs (end queue net) (if (null queue)
nil
(let ((path (car queue))) (let ((node (car path)))
(if (eql node end) (reverse path) (bfs end
(append (cdr queue)
(new-paths path node net)) net))))))
(defun new-paths (path node net) (mapcar #’(lambda (n)
(cons n path))
(cdr (assoc node net))))
Рис. 3.12. Поиск в ширину
|
b |
a |
d |
|
c |
Рис. 3.13. Простейшая сеть
68 |
Глава 3. Списки |
Программа на рис. 3.12 реализует поиск в ширину (breadth-first search). Каждый слой сети исследуется поочередно друг за другом, пока не бу дет найден нужный элемент или достигнут конец сети. Последователь ность исследуемых узлов представляется в виде очереди.
Приведенный на рис. 3.12 код слегка усложняет эту идею, позволяя не только прийти к пункту назначения, но еще и сохранить запись о том, как мы туда добрались. Таким образом, мы оперируем не с очередью уз лов, а с очередью пройденных путей.
Поиск выполняется функцией bfs. Первоначально в очереди только один элемент – путь к начальному узлу. Таким образом, shortest-path вызывает bfs с (list (list start)) в качестве исходной очереди.
Первое, что должна сделать bfs, – проверить, остались ли еще непрой денные узлы. Если очередь пуста, bfs возвращает nil, сигнализируя, что путь не был найден. Если же еще есть непроверенные узлы, bfs бе рет первый из очереди. Если car этого узла содержит искомый элемент, значит, мы нашли путь до него, и мы возвращаем его, предварительно развернув. В противном случае мы добавляем все дочерние узлы в ко нец очереди. Затем мы рекурсивно вызываем bfs и переходим к следую щему слою.
Так как bfs осуществляет поиск в ширину, то первый найденный путь будет одновременно самым коротким или одним из кратчайших, если есть и другие пути такой же длины:
> (shortest-path ’a ’d min) (A C D)
А вот как выглядит соответствующая очередь во время каждого из вы зовов bfs:
((A))
((B A) (C A)) ((C A) (C B A))
((C B A) (D C A)) ((D C A) (D C B A))
В каждой следующей очереди второй элемент предыдущей очереди ста новится первым, а первый элемент становится хвостом (cdr) любых но вых элементов в конце следующей очереди.
Разумеется, приведенный на рис. 3.12 код далек от полноценной эффек тивной реализации. Однако он убедительно показывает гибкость и удоб ство списков. В этой короткой программе мы используем списки тремя различными способами: список символов представляет путь, список пу тей представляет очередь1 при поиске по ширине, а ассоциативный спи сок изображает саму сеть целиком.
1В разделе 12.3 будет показано, как реализовать очереди более эффектив ным образом.