- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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
Более сложные вопросы
Эта глава необязательна. В ней описывается ряд эзотерических особен ностей языка. Common Lisp похож на айсберг: огромная часть его воз можностей не видна для большинства пользователей. Быть может, вам никогда не придется определять пакеты или макросы чтения самостоя тельно, но знание подобных моментов может сильно облегчить жизнь.
14.1. Спецификаторы типов
В Common Lisp типы не являются объектами. Так, к примеру, нет объ екта, соответствующего типу integer. То, что мы получаем при вызове type-of и передаем в качестве аргумента функции typep, является не ти пом, а спецификатором типа.
Спецификатор типа – это его имя. Простейшими спецификаторами ти пов являются символы типа integer. Они формируют иерархию типов во главе с типом t, к которому принадлежат все объекты. Иерархия типов не является деревом. Например, от nil наверх ведут два пути: один че рез atom, а второй через list и sequence.
На самом деле, тип – это просто множество объектов. Это означает, что количество типов, как и количество множеств объектов, может быть бесконечным. Некоторые типы обозначаются атомарно, например inte ger определяет множество целых чисел. Но мы можем также конструи ровать составные спецификаторы типов, которые ссылаются на любые множества объектов.
Например, пусть a и b – спецификаторы некоторых типов, тогда (or a b) обозначает объединение множеств объектов, соответствующих типам a и b. Таким образом, объект будет принадлежать типу (or a b), если он принадлежит типу a или типу b.
240 |
Глава 14. Более сложные вопросы |
Если бы circular? была функцией, возвращающей истину для цикличе ских по хвосту списков, то для определения набора правильных последо вательностей можно было бы использовать следующий спецификатор:1
(or vector (and list (not (satisfies circular?))))
Некоторые атомарные спецификаторы типов могут встречаться и в со ставных типах. Например, следующий спецификатор соответствует на бору целых чисел от 1 до 100 включительно:
(integer 1 100)
Втаких случаях говорят, что спецификатор определяет конечный тип.
Всоставном спецификаторе типа некоторые поля могут оставаться не определенными. Такое поле помечается * вместо параметра. Так,
(simple-array fixnum (* *))
описывает набор двумерных простых массивов, специализированных для fixnum, а
(simple-array fixnum *)
описывает множество (надтип предыдущего) простых массивов, спе циализированных для fixnum. Завершающие звездочки могут быть опу щены, поэтому последний спецификатор можно записать так:
(simple-array fixnum)
Если составной тип не содержит аргументов, то он эквивалентен ато марному. Таким образом, тип simple-array описывает множество всех простых массивов.
Чтобы постоянно не повторять громоздкие определения составных спе цификаторов, которые вы часто используете, для них можно опреде лить аббревиатуры с помощью deftype. Его использование напоминает defmacro, только он раскрывается не в выражение, а в спецификатор ти па. С помощью следующего определения:
(deftype proseq ()
’(or vector (and list (not (satisfies circular?)))))
мы создаем новый атомарный спецификатор proseq:
> (typep #(1 2) ’proseq) T
Если вы определите составной спецификатор с аргументами, то они не будут вычисляться, как и в случае defmacro. Так,
(deftype multiple-of (n)
‘(and integer (satisfies (lambda (x)
(zerop (mod x ,n))))))
1Несмотря на то, что этот факт обычно не упоминается в стандарте, вы мо жете использовать в спецификаторах типов выражения and и or с любым ко личеством аргументов, подобно макросам and и or.
14.2. Бинарные потоки |
241 |
определяет (multiple-of n) как спецификатор для всех множителей n:
> (typep 12 ’(multiple-of 4)) T
Спецификаторы типов интерпретируются и поэтому работают медлен но, так что в общем случае для подобных проверок лучше определить функцию.
14.2.Бинарные потоки
Вглаве 7 помимо потоков знаков упоминались также и бинарные пото ки. Бинарный поток – это источник и/или получатель не знаков, а це лых чисел. Для создания бинарного потока нужно задать подтип integer (чаще всего это unsigned-byte) как значение параметра :element-type при открытии потока.
Для работы с бинарными потоками имеются лишь две функции: readbyte и write-byte. Функцию для копирования содержимого файла мож но определить следующим образом:
(defun copy-file (from to)
(with-open-file (in from :direction :input :element-type ’unsigned-byte)
(with-open-file (out to :direction :output :element-type ’unsigned-byte)
(do ((i (read-byte in nil -1) (read-byte in nil -1)))
((minusp i)) (declare (fixnum i)) (write-byte i out)))))
Задавая тип unsigned-byte в качестве аргумента :element-type, вы указы ваете операционной системе тип элементов, для которых будет произво диться ввод-вывод. При желании работать конкретно, к примеру, с 7-бит ными числами, можно передать в :element-type
(unsigned-byte 7)
14.3.Макросы чтения
Вразделе 7.5 была представлена концепция макрознаков – знаков, имеющих особое значение для read. С каждым из них связана функция, указывающая read, что делать при встрече этого знака. Вы можете из менять определения функций, связанных с уже существующими мак рознаками, а также определять собственные макросы чтения.
Функция set-macro-character предоставляет один из способов определе ния макросов чтения. Она принимает знак и функцию, вызываемую read при встрече этого знака.
242 |
Глава 14. Более сложные вопросы |
Одним из старейших макросов чтения в Лиспе является ’, quote. Мы могли бы определить его следующим образом:
(set-macro-character #\’ #’(lambda (stream char)
(list (quote quote) (read stream t nil t))))
Когда read встречает ’ в обычном контексте, она возвращает результат вызова данной функции для текущего состояния потока и знака. (В дан ном случае функция игнорирует свой второй аргумент, так как он всегда является кавычкой.) Поэтому когда read увидит ’a, она вернет (quote a).
Теперь можно понять смысл последнего аргумента read. Он сообщает, произведен ли вызов read внутри другого вызова read. Следующие аргу менты read будут одинаковыми практически для всех макросов чтения: первый аргумент – поток; второй аргумент, t, сообщает, что при дости жении конца файла read будет сигнализировать об ошибке; третий ар гумент задает значение, возвращаемое вместо данной ошибки в случае истинности второго аргумента (в нашем примере он не применяется); четвертый аргумент, t, сообщает о рекурсивности вызова read.
Вы можете определять (с помощью make-dispatch-macro-character) свои собственные диспетчеризирующие макрознаки, но раз # уже определен для этой цели, можно пользоваться и им. Шесть комбинаций, начинаю щихся с #, явным образом зарезервированы для ваших нужд: #!, #?, #[,
#], #{ и #}.
Новую комбинацию с управляющим макрознаком можно определить
спомощью вызова функции set-dispatch-macro-character, которая схожа
сset-macro-character, но set-dispatch-macro-character принимает не один, а два знака. Следующий код определяет #? в качестве макроса чтения, возвращающего список целых чисел:
(setf-dispatch-macro-character #\# #\? #’(lambda (stream char1 char2)
(list ’quote
(let ((lst nil))
(dotimes (i (+ (read stream t nil t) 1)) (push i lst))
(nreverse lst)))))
Теперь #?n будет прочитан как список всех целых чисел от 0 до n. На пример:
> #?7 (0 1 2 3 4 5 6 7)
Второе место по распространенности после простых макрознаков зани мают знаки-ограничители списков. Еще одна комбинация, зарезерви рованная для пользователя, – #{. Вот пример определения более про двинутых скобок:
(set-macro-character #\} (get-macro-character #\)))