- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
10
Макросы
Лисп-код записывается в виде списков, которые сами являются Лиспобъектами. В разделе 2.3 говорилось о том, что эта особенность предо ставляет возможность писать программы, которые пишут другие про граммы. В этой главе показано, как переступить черту между выраже ниями и кодом.
10.1. Eval
Очевидно, что создавать выражения нужно путем вызова list. Однако возникает другой вопрос: как объяснить Лиспу, что созданный спи сок – это код? Для этого существует функция eval, вычисляющая за данное выражение и возвращающая его значение:
>(eval ’(+ 1 2 3))
6
>(eval ’(format t "Hello")) Hello
NIL
Да, именно об eval мы говорили все это время. Следующая функция реализует некое подобие toplevel:
(defun our-toplevel () (do ()
(nil)
(format t "~%> ") (format (eval (read)))))
По этой причине toplevel также называют read-eval-print loop (цикл чтение-вычисление-печать) .
Вызов eval – один из способов перешагнуть границу между списками и кодом. Однако это не лучший путь:
10.1. Eval |
171 |
1.Он неэффективен: eval получает простой список, и она вынуждена его либо скомпилировать, либо вычислить в интерпретаторе. Несо мненно, оба этих варианта намного медленнее, чем исполнение уже скомпилированного кода.
2.Выражение вычисляется вне какого-либо лексического контекста. Если, например, вызвать eval внутри let, выражения, переданные в eval, не смогут ссылаться на переменные, определенные в let.
Существуют гораздо более оптимальные способы генерации кода (см. описание в следующем разделе) . А что касается eval, то ее можно ис пользовать, пожалуй, лишь для создания чего-то вроде циклов toplevel.
Поэтому для программистов основная ценность eval заключается в том, что эта функция выражает концептуальную суть Лиспа. Можно счи тать, что eval определена как большое выражение cond:
(defun eval (expr env) (cond ...
((eql (car expr) ’quote) (cadr expr))
...
(t (apply (symbol-function (car expr)) (mapcar #’(lambda (x)
(eval x env)) (cdr expr))))))
Большинство выражений обрабатываются последним условием, кото рое говорит: взять функцию, имя которой записано в car, вычислить ар гументы из cdr и вернуть результат применения функции к хвосту (cdr) списка1.
Тем не менее для выражения вида (quote x) такой подход неприменим, поскольку вся суть quote в том, чтобы защитить свой аргумент от вы числения. Это значит, что нам необходимо специальное условие для quote. Так мы приходим к сути понятия «специальный оператор» – это оператор, для которого в eval имеется особое условие.
Функции coerce и compile также предоставляют возможность перейти от списков к коду. Лямбда-выражение можно превратить в функцию:
>(coerce ’(lambda (x) x) ’function) #<Interpreted-Function BF9D96>
Спомощью функции compile с первым аргументом nil также можно скомпилировать лямбда-выражение, переданное вторым аргументом:
>(compile nil ’(lambda (x) (+ x 2)))
#<Compiled-Function DF55BE>
1Как и реальная eval, наша функция имеет еще один аргумент (env), пред ставляющий лексическое окружение. Неточность нашей модели eval состо ит в том, что она получает функцию перед тем, как приступает к обработке аргументов, в то время как в Common Lisp порядок выполнения этих опера ций специальным образом не определен.
172 |
Глава 10. Макросы |
NIL
NIL
Поскольку coerce и compile могут принимать списки в качестве аргумен тов, программа может создавать новые функции на лету. Однако этот метод также нельзя назвать лучшим, так как он имеет те же недостат ки, что и eval.
Причина неэффективности eval, coerce и compile в том, что они пересе кают границу между списками и кодом и создают функции в момент выполнения (run-time). Пересечение границы – дорогое удовольствие. В большинстве случаев это лучше делать во время компиляции, а не в процессе выполнения программы. В следующем разделе будет показа но, как это сделать.
10.2. Макросы
Наиболее распространенный способ писать программы, которые будут писать программы, – это создание макросов. Макросы – это операторы, которые реализуются через трансформацию. Определяя макрос, вы ука зываете, как его вызов должен быть преобразован. Само преобразование автоматически выполняется компилятором и называется раскрытием макроса (macro-expansion). Код, полученный в результате раскрытия макроса, естественным образом становится частью программы, как ес ли бы он был набран вами.
Обычно макросы создаются с помощью defmacro. Вызов defmacro напо минает вызов defun, но вместо того чтобы описать, как должно быть по лучено значение, он указывает, как этот вызов должен быть преобразо ван. Например, макрос, устанавливающий значение своего аргумента в nil, может быть определен так:
(defmacro nil! (x) (list ’setf x nil))
Данный макрос создает новый оператор nil!, который принимает один аргумент. Вызов вида (nil! a) будет преобразован в (setf a nil) и лишь за тем скомпилирован или вычислен. Таким образом, если набрать (nil! x)
вtoplevel,
>(nil! x) NIL
>x
NIL
это в точности эквивалентно набору выражения (setf x nil).
Чтобы протестировать функцию, ее вызывают. Чтобы протестировать макрос, его раскрывают. Функция macroexpand–1 принимает вызов мак роса и возвращает результат его раскрытия:
10.3. Обратная кавычка |
173 |
> (macroexpand–1 ’(nil! x)) (SETF X NIL)
T
Макрос может раскрываться в другой макровызов. Когда компилятор (или toplevel) встречает такой макровызов, он раскрывает этот макрос до тех пор, пока в коде не останется ни одного другого макровызова.
Секрет понимания макросов в осознании их реализации. Они – не бо лее чем функции, преобразующие выражения. К примеру, если вы пе редадите выражение вида (nil! a) в следующую функцию:
(lambda (expr)
(apply #’(lambda (x) (list ’setf x nil)) (cdr expr)))
то она вернет (setf a nil). Применяя defmacro, вы выполняете похожую работу. Все, что делает macroexpand–1, когда встречает выражение, car которого соответствует имени одного из макросов, – это перенаправля ет это выражение в соответствующую функцию.
10.3. Обратная кавычка
Макрос чтения обратная кавычка (backquote) позволяет строить спи ски на основе специальных шаблонов. Она широко используется при определении макросов. Обычной кавычке на клавиатуре соответствует закрывающая прямая кавычка (апостроф), а обратной – открывающая. Ее называют обратной из-за ее наклона влево.
Обратная кавычка, использованная сама по себе, эквивалентна обыч ной кавычке:
> ‘(a b c) (A B C)
Как и обычная кавычка, обратная кавычка предотвращает вычисле ние выражения.
Преимуществом обратной кавычки является возможность выборочного вычисления частей выражения с помощью , (запятой) и ,@ (запятая-эт). Любое подвыражение, предваряемое запятой, будет вычислено. Соче тая обратную кавычку и запятую, можно строить шаблоны списков:
>(setf a 1 b 2)
2
>‘(a is ,a and b is ,b) (A IS 1 AND B IS 2)
Используя обратную кавычку вместо вызова list, мы можем записы вать определения макросов, которые будут выглядеть так же, как ре зультат их раскрытия. К примеру, оператор nil! может быть определен следующим образом: