- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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
Списки
Списки – это одна из базовых структур данных в Лиспе. В ранних диа лектах списки были единственной структурой данных, и именно им Лисп обязан своим названием: «LISt Processor» (Обработчик списков) . Нынешний Лисп уже не соответствует этому акрониму. Common Lisp является языком общего назначения и предоставляет программисту широкий набор структур данных.
Процесс разработки Лисп-программ часто напоминает эволюцию само го Лиспа. В первоначальной версии программы вы можете использовать множество списков, однако в ее более поздних версиях целесообразнее будет перейти к использованию более специализированных и эффек тивных структур данных. В этой главе подробно описываются операции со списками, а также с их помощью поясняются некоторые общие кон цепции Лиспа.
3.1. Ячейки
В разделе 2.4 вводятся cons, car и cdr – простейшие функции для мани пуляций со списками. В действительности, cons объединяет два объек та в один, называемый ячейкой (cons). Если быть точнее, то cons – это пара указателей, первый из которых указывает на car, второй – на cdr.
С помощью cons-ячеек удобно объединять в пару объекты любых типов, в том числе и другие ячейки. Именно благодаря такой возможности с помощью cons можно строить произвольные списки.
Незачем представлять каждый список в виде cons-ячеек, но нужно пом нить, что они могут быть заданы таким образом. Любой непустой список может считаться парой, содержащей первый элемент списка и осталь ную его часть. В Лиспе списки являются воплощением этой идеи. Имен но поэтому функция car позволяет получить первый элемент списка,
3.1. Ячейки |
49 |
а cdr – его остаток (который является либо cons-ячейкой, либо nil). И до говоренность всегда была таковой: использовать car для обозначения первого элемента списка, а cdr – для его остатка. Так эти названия ста ли синонимами операций first и rest. Таким образом, списки – это не отдельный вид объектов, а всего лишь набор связанных между собой cons-ячеек.
Если мы попытаемся использовать cons вместе с nil,
> (setf x (cons ’a nil))
(A)
то получим список, состоящий из одной ячейки, как показано на рис. 3.1. Такой способ изображения ячеек называется блочным, потому что каж дая ячейка представляется в виде блока, содержащего указатели на car и cdr. Вызывая car или cdr, мы получаем объект, на который указывает соответствующий указатель:
>(car x)
A
>(cdr x) NIL
nil
a
Рис. 3.1. Список, состоящий из одной ячейки
Составляя список из нескольких элементов, мы получаем цепочку ячеек:
> (setf y (list ’a ’b ’c)) (A B C)
Эта структура показана на рис. 3.2. Теперь cdr списка будет указывать на список из двух элементов:
> (cdr y) (B C)
nil
a |
b |
c |
Рис. 3.2. Список из трех ячеек
50 |
Глава 3. Списки |
Для списка из нескольких элементов указатель на car дает первый эле мент списка, а указатель на cdr – его остаток.
Элементами списка могут быть любые объекты, в том числе и другие списки:
> (setf z (list ’a (list ’b ’c) ’d)) (A (B C) D)
Соответствующая структура показана на рис. 3.3; car второй ячейки указывает на другой список:
> (car (cdr z)) (B C)
nil
a |
|
|
|
|
|
|
|
|
nil |
d |
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bc
Рис. 3.3. Вложенный список
В этих двух примерах списки состояли из трех элементов. При этом в последнем примере один из элементов тоже является списком. Такие списки называют вложенными (nested), в то время как списки, не со держащие внутри себя подсписков, называют плоскими (flat).
Проверить, является ли объект cons-ячейкой, можно с помощью функ ции consp. Поэтому listp можно определить так:
(defun out-listp (x)
(or (null x) (consp x)))
Теперь определим предикат atom, учитывая тот факт, что атомами в Лис пе считается все, кроме cons-ячеек:
(defun our-atom (x) (not (consp x)))
Исключением из этого правила считается nil, который является и ато мом, и списком одновременно.
3.2. Равенство
Каждый раз, когда вы вызываете функцию cons, Лисп выделяет память для двух указателей. Это означает, что, вызывая cons дважды с одними и теми же аргументами, мы получим два значения, которые будут вы глядеть идентично, но соответствовать разным объектам:
3.3. Почему в Лиспе нет указателей |
51 |
> (eql (cons ’a nil) (cons ’a nil)) NIL
Функция eql1 возвращает t (true), только если сравниваемые значения соответствуют одному объекту в памяти Лиспа.
>(setf x (cons ’a nil))
(A)
>(eql x x)
T
Для проверки идентичности списков (и других объектов) используется предикат equal. С его точки зрения два одинаково выглядящих объекта равны:
> (equal x (cons ’a nil)) T
Покажем, как можно определить функцию equal для частного случая проверки на равенство списков2, предполагая, что если два объекта рав ны для предиката eql, то они будут равны и для equal:
(defun our-equal (x y) (or (eql x y)
(and (consp x) (consp y)
(our-equal (car x) (car y)) (our-equal (cdr x) (cdr y)))))
3.3. Почему в Лиспе нет указателей
Чтобы понять, как устроен Лисп, необходимо осознать, что механизм присваивания значения переменным похож на построение списков из объектов. Переменной соответствует указатель на ее значение, так же как cons-ячейки имеют указатели на car и cdr.
Некоторые другие языки позволяют оперировать указателями явно. Лисп же выполняет всю работу с указателями самостоятельно. Мы уже видели, как это происходит, на примере списков. Нечто похожее проис ходит и с переменными. Пусть две переменные указывают на один и тот же список:
>(setf x ’(a b c)) (A B C)
>(setf y x)
(A B C)
1В ранних диалектах Лиспа задачу eql выполнял eq. В Common Lisp eq – бо лее строгая функция, а основным предикатом проверки идентичности яв ляется eql. Роль eq разъясняется на стр. 234.
2Функция our-equal применима не к любым спискам, а только к спискам сим волов. Неточность в оригинале указана Биллом Стретфордом. – Прим. перев.
52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Глава 3. Списки |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
x = |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
y = |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nil |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
a |
|
|
|
|
b |
|
|
|
|
c |
|||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Рис. 3.4. Две переменные, указывающие на один список
Что происходит, когда мы пытаемся присвоить y значение x? Место в па мяти, связанное с переменной x, содержит не сам список, а указатель на него. Чтобы присвоить переменной y то же значение, достаточно просто скопировать этот указатель (рис. 3.4). В данном случае две переменные будут одинаковыми с точки зрения eql:
> (eql x y) T
Таким образом, в Лиспе указатели явно не используются, потому что любое значение, по сути, является указателем. Когда вы присваиваете значение переменной или сохраняете его в какую-либо структуру дан ных, туда, на самом деле, записывается указатель. Когда вы запраши ваете содержимое какой-либо структуры данных или значение перемен ной, Лисп возвращает данные, на которые ссылается указатель. Но это происходит неявно, поэтому вы можете записывать значения в структу ры или «в» переменные, не задумываясь о том, как это происходит.
Из соображений производительности Лисп взамен указателей иногда ис пользует непосредственное представление данных. Например, неболь шие целые числа занимают не больше места, чем указатель, поэтому некоторые реализации Лиспа оперируют непосредственно целыми чис лами, а не указателями на них. Но для программиста важно лишь то, что в Лиспе по умолчанию можно положить что угодно куда угодно. И если вы явно не объявили обратного, то можно записать любой вид объекта в любую структуру данных, включая и саму структуру в себя.
x = |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
nil |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
a |
|
|
|
|
b |
|
|
|
|
c |
y = nil
Рис. 3.5. Результат копирования