- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
174 |
Глава 10. Макросы |
(defmacro nil! (x) ‘(setf ,x nil))
Запятая-эт действует похожим образом, но вставляет поэлементно свой аргумент (который должен быть списком) в середину другого списка:
>(setf lst ’(a b c)) (A B C)
>‘(lst is ,lst) (LST IS (A B C))
>‘(its elements are ,@lst) (ITS ELEMENTS ARE A B C)
Это может оказаться полезным для макросов, которые используют ос таточный аргумент, например, для представления тела кода. Предполо жим, мы хотим создать макрос while, который вычисляет свои аргумен ты до тех пор, пока проверочное выражение остается истинным:
> (let ((x 0)) (while (< x 10)
(princ x) (incf x)))
0123456789 NIL
Мы можем определить такой макрос, используя остаточный параметр, который соберет все выражения тела макроса в список, а затем этот список будет встроен в результат раскрытия макроса благодаря ,@:
(defmacro while (test &rest body) ‘(do ()
((not ,test)) ,@body))
10.4. Пример: быстрая сортировка
На рис. 10.1 приведен пример1 функции, полностью построенной на мак росах, – функции, выполняющей сортировку векторов с помощью алго ритма быстрой сортировки (quicksort).°
Алгоритм быстрой сортировки выполняется следующим образом:
1.Один из элементов принимается в качестве опорного. Многие реали зации выбирают элемент из середины вектора.
2.Затем производится разбиение вектора, при этом его элементы пере ставляются местами до тех пор, пока все элементы, меньшие опорно го, не будут находиться по левую сторону от больших или равных ему.
3.Наконец, если хотя бы одна из частей состоит из двух и более элемен тов, алгоритм применяется рекурсивно к каждому из сегментов.
1Код на рис. 10.1 содержит слегка исправленный код. Ошибка найдена Дже дом Кросби. – Прим. перев.
10.5. Проектирование макросов |
175 |
|
|
|
|
|
(defun quicksort (vec l r) |
|
|
(let ((i l) |
|
|
(j r) |
|
|
(p (svref vec (round (+ l r) 2)))) |
; 1 |
|
(while (<= i j) |
; 2 |
|
(while (< (svref vec i) p) (incf i)) |
|
|
(while (> (svref vec j) p) (decf j)) |
|
|
(when (<= i j) |
|
|
(rotatef (svref vec i) (svref vec j)) |
|
|
(incf i) |
|
|
(decf j))) |
|
|
(if (>= (- j l) 1) (quicksort vec l j)) |
; 3 |
|
(if (>= (- r i) 1) (quicksort vec i r))) |
|
|
vec) |
|
|
|
|
Рис. 10.1. Быстрая сортировка
С каждым последующим вызовом сортируемый список становится ко роче, и так до тех пор, пока весь список не будет отсортирован.
Реализация этого алгоритма (рис. 10.1) требует указания вектора и двух чисел, задающих диапазон сортировки. Элемент, находящийся в сере дине этого диапазона, выбирается в качестве опорного элемента (p). За тем по мере продвижения вдоль вектора к окончанию диапазона произ водятся перестановки его элементов, которые либо слишком большие, чтобы находиться слева, либо слишком малые, чтобы находиться спра ва. (Перестановка двух элементов осуществляется с помощью rotatef.) Далее эта процедура повторяется рекурсивно для каждой полученной части, содержащей более одного элемента.
Помимо макроса while, определенного в предыдущем разделе, в quicksort (рис. 10.1) задействованы встроенные макросы when, incf, decf и rotatef. Их использование существенно упрощает код, делая его аккуратным
ипонятным.
10.5.Проектирование макросов
Написание макросов – это отдельная разновидность программирова ния, имеющая собственные уникальные цели и проблемы. Умение из менять поведение компилятора отчасти похоже на возможность изме нять сам компилятор. Поэтому при разработке макросов нужно мыс лить как создатель языка.
В этом разделе будут рассмотрены основные трудности, связанные с на писанием макросов, и методики их разрешения. В качестве примера мы создадим макрос ntimes, вычисляющий свое тело n раз:
> (ntimes 10 (princ "."))
176 Глава 10. Макросы
..........
NIL
Ниже приведено некорректное определение макроса ntimes, иллюстри рующее некоторые проблемы, связанные с проектированием макросов:
(defmacro ntimes (n &rest body) |
; wrong |
‘(do ((x 0 (+ x 1))) |
|
((>= x ,n)) |
|
,@body)) |
|
На первый взгляд такое определение кажется вполне корректным. Для приведенного выше примера оно будет работать нормально, однако на самом деле в нем есть две проблемы.
Первой проблемой, о которой должны думать разработчики макросов, является непреднамеренный захват переменных. Это случается, когда имя переменной, используемой в раскрытии макроса, совпадает с име нем переменной, которая уже существует в том окружении, куда встав ляется раскрытие макроса. Некорректное определение ntimes создает переменную с именем x. Поэтому если макрос вызывается там, где уже существует переменная с таким же именем, результат его выполнения может не соответствовать нашим ожиданиям:
> (let ((x 10)) (ntimes 5
(setf x (+ x 1)))
x)
10
Мы предполагали, что значение x будет увеличено в пять раз и в итоге будет равно 15. Но поскольку переменная x используется еще и внутри макроса как итерируемая переменная в do, setf будет увеличивать зна чение этой переменной, а не той, что предполагалось. Раскрыв этот макрос, мы увидим, что предыдущее выражение выглядит следующим образом:
(let ((x 10))
(do ((x 0 (+ x 1))) ((> x 5))
(setf x (+ x 1)))
x)
Наиболее общим решением будет не использовать обычные символы в тех местах, где они могут быть захвачены. Вместо этого можно исполь зовать gensym (см. раздел 8.4). В связи с тем что read интернирует каж дый встреченный символ, gensym никоим образом не будет равен (с точ ки зрения eql) любому другому символу, встречающемуся в тексте про граммы. Переписав наше определение ntimes с использованием gensym вместо x, мы сможем избавиться от вышеописанной проблемы:
(defmacro ntimes (n &rest body) |
; wrong |
(let ((g (gensym))) |
|
10.5. Проектирование макросов |
177 |
‘(do ((,g 0 (+ ,g 1))) ((>= ,g ,n))
,@body)))
Тем не менее в данном определении по-прежнему кроется другая про блема: повторное вычисление. Поскольку первый аргумент встраивает ся непосредственно в вызов do, он будет вычисляться на каждой итера ции. Эта ошибка ясно демонстрируется примером, в котором первое вы ражение содержит побочные эффекты:
> (let ((v 10))
(ntimes (setf v (- v 1)) (princ ".")))
.....
NIL
Поскольку начальное значение v равно 10 и setf возвращает значение своего второго аргумента, мы ожидали получить девять точек, однако в действительности их только пять.
Чтобы разобраться с причиной такого эффекта, посмотрим на результат раскрытия макроса:
(let ((v 10))
(do ((#:g1 0 (+ #:g1 1)))
((>= #:g1 (setf v (- v 1)))) (princ ".")))
На каждой итерации мы сравниваем значение переменной (gensym при печати обычно предваряется #:) не с числом 9, а с выражением, которое уменьшается на единицу при каждом вызове.
Нежелательных повторных вычислений можно избежать, вычислив значение выражения перед началом цикла и записав его в переменную. Обычно для этого требуется еще один gensym:
(defmacro ntimes (n &rest body) (let ((g (gensym))
(h (gensym))) ‘(let ((,h ,n))
(do ((,g 0 (+ ,g 1))) ((>= ,g ,h))
,@body))))
Эта версия ntimes является полностью корректной.
Непреднамеренные повторные вычисления и захват переменных – ос новные проблемы, возникающие при разработке макросов, но есть и другие. С опытом видеть ошибки в макросах станет так же легко и ес тественно, как предупреждать деление на ноль. Но поскольку макросы наделяют нас новыми возможностями, то и сложности, с которыми нам придется столкнуться, тоже новы.
Ваша реализация Common Lisp содержит множество примеров, на кото рых вы можете поучиться проектировать макросы. Раскрывая вызовы