- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
14.4. Пакеты |
243 |
(set-dispatch-macro-character #\# #\{ #’(lambda (stream char1 char2)
(let ((accum nil)
(pair (read-delimited-list #\} stream t))) (do ((i (car pair) (+ i 1)))
((> i (cadr pair))
(list ’quote (nreverse accum))) (push i accum)))))
Они задают чтение выражения #{x y} в качестве списка целых чисел от x до y включительно:
> #{2 7} (2 3 4 5 6 7)
Функция read-delimited-list определена специально для таких макро сов чтения. Ее первым аргументом является знак, который расценива ется как конец списка. Чтобы знак } был опознан как разграничитель, необходимо предварительно сообщить об этом с помощью set-macro-cha racter.
Чтобы иметь возможность пользоваться макросом чтения в файле, в ко тором определен сам макрос, его определение должно быть завернуто в выражение eval-when с целью обеспечить его выполнение в момент ком пиляции. В противном случае определение будет скомпилировано, но не вычислено до тех пор, пока скомпилированный файл не будет загружен.
14.4. Пакеты
Пакет – это объект Лиспа, сопоставляющий именам символы. Текущий пакет всегда хранится в глобальной переменной *package*. При запуске Common Lisp стартовым пакетом является common-lisp-user, неформаль но известный также как пользовательский пакет. Функция package- name возвращает имя текущего пакета, а find-package возвращает пакет
сзаданным именем:
>(package-name *package*) "COMMON-LISP-USER"
>(find-package "COMMON-LISP-USER") #<Package "COMMON-LISP-USER" 4CD15E>
Обычно символ интернируется в пакет, являющийся текущим на мо мент его чтения. Функция symbol-package принимает символ и возвра щает пакет, в который он был интернирован.
> (symbol-package ’sym)
#<Package "COMMON-LISP-USER" 4CD15E>
Интересно, что это выражение вернуло именно такое значение, посколь ку оно должно было быть считано перед вычислением, а само считыва ние привело к интернированию sym. Для последующего применения присвоим sym некоторое значение:
244 |
Глава 14. Более сложные вопросы |
> (setf sym 99) 99
Теперь мы создадим новый пакет и переключимся в него:
> (setf *package* (make-package ’mine
:use ’(common-lisp)))
#<Package "MINE" 63390E>
В этот момент должна зазвучать зловещая музыка, ибо теперь мы в ином мире, где sym перестал быть тем, чем был раньше:
MINE> sym
Error: SYM has no value.
Почему это произошло? Чуть раньше мы установили sym в 99, но сдела ли это в другом пакете, а значит, для другого символа, нежели sym в па кете mine.1 Чтобы сослаться на исходный sym из другого пакета, необхо димо его предварить именем его родного пакета и парой двоеточий:
MINE> common-lisp-user::sym 99
Итак, несколько символов с одинаковыми именами могут сосущество вать, если находятся в разных пакетах. Один символ находится в паке те common-lisp-user, другой – в mine, и это разные символы. В этом заклю чается весь смысл пакетов. Поместив свою программу в отдельный па кет, вы можете не переживать, что кто-то использует выбранные вами имена функций и переменных где-то еще. Даже если где-либо будет ис пользовано такое же имя, как у вас, это будет уже другой символ.
Пакеты также предоставляют возможность сокрытия информации. Программы должны ссылаться на функции и переменные по их име нам. Если вы не делаете какое-либо имя доступным вне своего пакета, то вряд ли функции из других пакетов смогут использовать и изменять то, на что это имя ссылается.
Использование пары двоеточий для обозначения символа не из текуще го пакета считается плохим стилем. Фактически вы нарушаете модуль ность, которая устанавливается пакетами. Если вам приходится ис пользовать двойное двоеточие для ссылки на символ, это означает, что кто-то не хочет, чтобы вы на него ссылались.
Обычно следует ссылаться лишь на экспортированные символы. Если мы вернемся назад в пользовательский пакет (in-package устанавливает *package*) и экспортируем символ, интернированный в нем,
MINE> (in-package common-lisp-user) #<Package "COMMON-LISP-USER" 4CD15E> > (export ’bar)
T
1Некоторые реализации Common Lisp печатают имя пакета перед приглаше нием toplevel, когда вы находитесь не в пользовательском пакете.
14.4. Пакеты |
245 |
> (setf bar 5) 5
то сделаем его видимым и в других пакетах. Теперь, вернувшись в па кет mine, мы сможем ссылаться на bar через одинарное двоеточие, по скольку отныне это публично доступное имя:
> (in-package mine) #<Package "MINE" 63390E> MINE> common-lisp-user:bar 5
Кроме того, мы можем сделать еще один шаг и импортировать символ bar в текущий пакет, и тогда пакет mine будет делить его с пользователь ским пакетом:
MINE> (import ’common-lisp-user:bar) T
MINE> bar 5
На импортированный символ можно ссылаться без какого-либо пре фикса. Оба пакета теперь делят этот символ, а значит, уже не может быть отдельного символа mine:bar.
А что произойдет, если такой символ уже был? В таком случае импорт вызовет ошибку, подобную той, которую мы увидим, попытавшись им портировать sym:
MINE> (import ’common-lisp-user::sym)
Error: SYM is already present in MINE.
Ранее мы предпринимали неудачную попытку вычислить sym в mine, ко торая привела к тому, что этот символ был интернирован в mine. У него не было значения, и это вызвало ошибку, но интернирование все равно произошло, просто потому, что он был набран и считан, когда текущим пакетом был mine. Поэтому теперь, когда мы пытаемся импортировать sym в mine, символ с таким именем уже есть в этом пакете.
Другой способ получить доступ к символам другого пакета – использо вать сам пакет:
MINE> (use-package ’common-lisp-user)
T
Теперь все символы, экспортированные пользовательским пакетом, при надлежат mine. (Если sym экспортировался пользовательским пакетом, то этот вызов приведет к ошибке.)
common-lisp – это пакет, содержащий имена всех встроенных операторов и функций. Так как мы передали имя этого пакета аргументу :use вызо ва make-package, который создал пакет mine, в нем будут видимы все име на Common Lisp:
246 Глава 14. Более сложные вопросы
MINE> #’cons
#<Compiled-Function CONS 462A3E>
Как и компиляция, операции с пакетами редко выполняются непо средственно в toplevel. Гораздо чаще их вызовы содержатся в файлах с исходным кодом. Обычно достаточно поместить в начале файла def package и in-package, как на стр. 148.
Модульность, создаваемая пакетами, несколько своеобразна. Она дает нам модули не объектов, а имен. Каждый пакет, использующий commonlisp, имеет доступ к символу cons, поскольку в пакете common-lisp зада на функция с таким именем. Но как следствие этого и переменная с именем cons будет видима во всех пакетах, использующих common-lisp. Если пакеты сбивают вас с толку, то основная причина этого в том, что они основываются не на объектах, а на их именах.°
14.5. Loop
Макрос loop первоначально был разработан в помощь начинающим Лисп-программистам для написания итеративного кода, позволяя ис пользовать для этого выражения, напоминающие обычные английские фразы, которые затем транслируются в Лисп-код. К несчастью, loop оказался похож на английский язык в большей степени, чем предпола гали его создатели: в несложных случаях вы можете использовать его, совершенно не задумываясь о том, как это работает, но понять общий принцип его действия практически невозможно.
Если вы желаете ознакомиться с loop за один день, то для вас есть две новости: хорошая и плохая. Хорошая заключается в том, что вы не оди ноки: мало кто в действительности понимает, как он работает. Плохая же состоит в том, что вы, вероятно, никогда этого не поймете, хотя бы потому, что стандарт ANSI фактически не предоставляет формального описания его поведения.
Единственным определением loop является его реализация, а единст венным способом изучить его (насколько это возможно) являются при меры. В стандарте ANSI в главе, описывающей loop, имеется большой набор примеров, и мы применим здесь такой же подход, чтобы познако мить вас с основными концепциями этого макроса.
Первое, что люди замечают в макросе loop, – это наличие у него син таксиса. Его тело состоит не из подвыражений, а из предложений, ко торые не разделяются скобками. Каждое предложение имеет свой син таксис. В целом, loop-выражения напоминают Алгол-подобные языки. Но есть одно серьезное отличие, сильно отдаляющее loop от Алгола: по рядок, в котором следуют предложения, весьма слабо определяет поря док, в котором они будут исполняться.
Вычисление loop-выражения осуществляется в три этапа, причем каж дое предложение может вносить свой вклад более чем в один из них. Вот эти этапы:
14.5. Loop |
247 |
1.Пролог. Выполняется один раз за весь вызов loop; устанавливает ис ходные значения переменных.
2.Тело. Вычисляется на каждой итерации. Начинается с проверок на завершение, следом за ними идут вычисляемые выражения, после которых обновляются итерируемые переменные.
3.Эпилог. Вычисляется по завершении всех итераций; определяет воз вращаемое значение.
Рассмотрим некоторые примеры loop-выражений и прикинем, к каким этапам относятся их части. Один из простейших примеров:
> (loop for x from 0 to 9 do (princ x))
0123456789 NIL
Данное выражение печатает целые числа от 0 до 9 и возвращает nil. Первое предложение:
for x from 0 to 9
вносит вклад в два первых этапа: устанавливает значение переменной x
в0 в прологе, сравнивает с 9 в начале тела и увеличивает переменную
вконце. Второе предложение
do (princ x)
добавляет код (princ-выражение) в тело.
В более общем виде предложение с for определяет форму установления исходного значения и форму обновления. Завершение же может управ ляться чем-нибудь типа while или until:
> (loop for x = 8 then (/ x 2) until (< x 1)
do (princ x))
8421 NIL
С помощью and вы можете создавать составные конструкции с for, со держащие несколько переменных и обновляющие их параллельно:
> (loop for x from 1 to 4 and y from 1 to 4
do (princ (list x y))) (1 1) (2 2) (3 3) (4 4)
NIL
А если предложений с for будет несколько, то переменные будут обнов ляться поочередно.
Еще одной типичной задачей, решаемой во время итерации, является накопление каких-либо значений. Например:
248 |
Глава 14. Более сложные вопросы |
> (loop for x in ’(1 2 3 4) collect (1+ x))
(2 3 4 5)
При использовании in вместо from в for-предложении переменная по очереди принимает значения каждого из элементов списка, а не после довательных целых чисел.
Вприведенном примере collect оказывает влияние на все три этапа.
Впрологе создается анонимный аккумулятор, имеющий значение nil; в теле (1+ x) к аккумулятору прибавляется 1, а в эпилоге возвращается значение аккумулятора.
Это наш первый пример, возвращающий какое-то конкретное значе ние. Существуют предложения для явного задания возвращаемого зна чения, но в их отсутствие этим занимается collect. Фактически эту же работу мы могли бы выполнить через mapcar.
Наиболее часто loop используется для сбора результатов вызова некото рой функции определенное количество раз:
> (loop for x from 1 to 5 collect (random 10))
(3 8 6 5 0)
В данном примере мы получили список из пяти случайных чисел. Именно для этой цели мы уже определяли функцию map-int (стр. 117). Тогда зачем же мы это делали, если у нас есть loop? Но точно так же можно спросить: зачем нам нужен loop, когда у нас есть map-int?°
С помощью collect можно также аккумулировать значения в именован ную переменную. Следующая функция принимает список чисел и воз вращает списки его четных и нечетных элементов:
(defun even/odd (ns) (loop for n in ns
if (evenp n)
collect n into evens else collect n into odds
finally (return (values evens odds))))
Предложение finally добавляет код в эпилог. В данном случае оно опре деляет возвращаемое выражение.
Предложение sum похоже на collect, но накапливает значения в виде числа, а не списка. Сумму всех чисел от 1 до n мы можем получить так:
(defun sum (n)
(loop for x from 1 to n sum x))
Более детальное рассмотрение loop содержится в приложении D, начи ная со стр. 331. Приведем несколько примеров: рис. 14.1 содержит две итеративные функции из предыдущих глав, а на рис. 14.2 показана аналогичная их реализация с помощью loop.
14.5. Loop |
249 |
(defun most (fn lst) (if (null lst)
(values nil nil)
(let* ((wins (car lst))
(max (funcall fn wins))) (dolist (obj (cdr lst))
(let ((score (funcall fn obj))) (when (> score max)
(setf wins obj
max score)))) (values wins max))))
(defun num-year (n) (if (< n 0)
(do* ((y (- yzero 1) (- y 1))
(d (- (year-days y)) (- d (year-days y)))) ((<= d n) (values y (- n d))))
(do* ((y yzero (+ y 1)) (prev 0 d)
(d (year-days y) (+ d (year-days y)))) ((> d n) (values y (- n prev))))))
Рис. 14.1. Итерация без loop
Одно loop-предложение может ссылаться на переменные, устанавливае мые другим. Например, в определении even/odd предложение finally ссылается на переменные, установленные в двух предыдущих collectпредложениях. Отношения между такими переменными являются од ним из самых неясных моментов при использовании loop. Сравните два выражения:
(loop for y = 0 then z for x from 1 to 5 sum 1 into z
finally (return (values y z)))
(loop for x from 1 to 5 for y = 0 then z sum 1 into z
finally (return (values y z)))
Они содержат всего четыре предложения и потому кажутся довольно простыми. Возвращают ли они одинаковые значения? Какие значения они вернут? Попытки найти ответ в стандарте языка окажутся тщет ными. Каждое предложение внутри loop само по себе незатейливо, но сочетаться они могут довольно причудливым и в конечном итоге четко не заданным образом.
По этой причине использовать loop не рекомендуется. Самое лучшее, что можно сказать об этом макросе, – это то, что в типичных случаях,