- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
148 |
Глава 8. Символы |
Пусть, например, имеется программа, разделенная на два пакета, math и disp. Если символ fft экспортируется пакетом math, то в пакете disp он будет доступен как math:fft. Внутри пакета math к нему можно обра щаться без префикса – просто fft.
Ниже приводится пример определения пакета, которое можно помес тить в начало файла:
(defpackage "MY-APPLICATION"
(:use "COMMON-LISP" "MY-UTILITIES") (:nicknames "APP")
(:export "WIN" "LOSE" "DRAW"))
(in-package my-application)
Для создания нового пакета используется defpackage. Пакет my-applica tion1 использует два других пакета, common-lisp и my-utilities. Это озна чает, что символы, экспортируемые ими, доступны в новом пакете без использования префиксов. Практически всегда в создаваемых пакетах используется common-lisp, поскольку никто не хочет использовать ква лифицированное имя с префиксом при каждом обращении к встроен ным в Лисп операторам и переменным.
Пакет my-applications сам экспортирует лишь три символа: win, lose и draw. Поскольку в defpackage было также задано сокращенное имя (nickname) пакета – app, то и к экспортируемым символам можно будет обращаться также и через него: app:win.
За defpackage следует вызов in-package, которая выставляет текущим пакетом my-application. Теперь все новые символы, для которых не ука зан пакет, будут интернироваться в my-application до тех пор, пока inpackage не изменит текущий пакет. После окончания загрузки файла, содержащего вызов in-package, значение текущего пакета сбрасывается
вто, каким оно было до начала загрузки.
8.6.Ключевые слова
Символы пакета keyword (известные как ключевые слова) имеют две осо бенности: они всегда самовычисляемы и вы можете всегда ссылаться на них просто как на :x вместо keyword:x. Когда мы только начинали ис пользоватьаргументыпоключу(стр.60),вызов(member ’(a) ’((a) (z)) test: #’equal) мог показаться нам более естественным, чем (member ’(a) ’((a) (z)) :test #’equal). Теперь мы понимаем, почему второе, слегка неестест венное написание, является корректным. Двоеточие перед именем сим вола позволяет отнести его к ключевым словам.
1В этом примере используются имена, состоящие лишь из заглавных букв, потому что, как упомянуто в разделе 8.1, имена символов конвертируются в верхний регистр.
8.7. Символы и переменные |
149 |
Зачем использовать ключевые слова вместо обычных символов? Пото му что они доступны везде. Функция, принимающая символы в качест ве аргументов, как правило, должна быть рассчитана на обработку именно ключевых слов. Например, следующий код может быть вызван из любого пакета без изменений:
(defun noise (animal) (case animal
(:dog :woof) (:cat :meow) (:pig :oink)))
Если бы эта функция использовала обычные символы, то она могла бы вызываться лишь в исходном пакете до тех пор, пока не будут экспорти рованы все символы-ключи case-выражения.
8.7. Символы и переменные
В Лиспе есть определенная особенность, которая может смущать но вичков: символы могут быть связаны с переменными двумя различны ми способами. Если символ является именем специальной переменной, ее значение хранится в одном из полей структуры символа (см. рис. 8.1). Получить доступ к этому полю можно с помощью symbol-value. Таким образом, существует прямая связь между символом и представляемой им специальной переменной.
С лексическими переменными дело обстоит иначе. Символ, используе мый в качестве лексической переменной, всего лишь заменяет соответ ствующее ему значение. Компилятор будет заменять ссылку на лекси ческую переменную регистром или областью памяти. В полностью скомпилированном коде не будет каких-либо упоминаний об этом сим воле (разумеется, если не включена соответствующая опция отладки) . Ни о какой связи между символом и значением лексической перемен ной говорить не приходится: к тому времени, как появляются значе ния, имена символов исчезают.
8.8. Пример: генерация случайного текста
Если вам предстоит написать программу, каким-либо образом работаю щую со словами, отличной идеей часто является использование симво лов вместо строк, потому что символы атомарны. Они могут сравнивать ся за одну итерацию с помощью eql, в то время как строки сравнивают ся побуквенно с помощью string-equal или string=. В качестве примера рассмотрим генерацию случайного текста. Первая часть программы бу дет считывать образец текста (чем он больше, тем лучше), собирая ин формацию о связях между соседними словами. Вторая ее часть будет случайным образом выполнять проходы по сети, построенной ранее из слов исходного текста. После прохождения каждого слова программа
150 |
Глава 8. Символы |
будет делать взвешенный случайный шаг к следующему слову, встре тившемуся после данного в оригинальном тексте. Полученный таким образом текст будет местами казаться довольно связным, потому что каждая пара слов в нем соотносится друг с другом. Поразительно, но це лые предложения, а иногда даже и абзацы будут порой казаться вполне осмысленными.
На рис. 8.2 приводится первая часть программы – код для сбора инфор мации из исходного текста. На его основе строится хеш-таблица *words*. Ключами в ней являются символы, соответствующие словам, а значе ниями будут ассоциативные списки следующего типа:
((|sin| . 1) (|wide| . 2) (|sights| . 1))
(defparameter *words* (make-hash-table :size 10000))
(defconstant maxword 100)
(defun read-text (pathname)
(with-open-file (s pathname :direction :input) (let ((buffer (make-string maxword))
(pos 0))
(do ((c (read-char s nil :eof) (read-char s nil :eof)))
((eql c :eof))
(if (or (alpha-char-p c) (char= c #\’)) (progn
(setf (aref buffer pos) c) (incf pos))
(progn
(unless (zerop pos)
(see (intern (string-downcase
(subseq buffer 0 pos))))
(setf pos 0)) (let ((p (punc c)))
(if p (see p)))))))))
(defun punc (c) (case c
(#\. ’|.|) (#\, ’|,|) (#\; ’|;|) (#\! ’|!|) (#\? ’|?|) ))
(let ((prev ‘|.|)) (defun see (symb)
(let ((pair (assoc symb (gethash prev *words*)))) (if (null pair)
(push (cons symb 1) (gethash prev *words*)) (incf (cdr pair))))
(setf prev symb)))
Рис. 8.2. Чтение образца текста
8.8. Пример: генерация случайного текста |
151 |
Выше приведено значение ключа |discover| для отрывка из «Paradise Lost» Мильтона1. Мы видим, что слово «discover» было использовано в поэме четыре раза: по одному разу перед словами «sin» и «sights» и два жды перед словом «wide». Подобная информация собирается функцией read-text, которая строит такой ассоциативный список для каждого сло ва в заданном тексте. При этом файл читается побуквенно, а слова соби раются в строку buffer. С параметром maxword=100 программа сможет чи тать слова, состоящие не более чем из ста букв. Для английских слов этого более чем достаточно.
Если считанный знак является буквой (это определяется с помощью alpha-char-p) или апострофом, то он записывается в буфер. Любой дру гой символ сигнализирует об окончании слова, после чего все накоп ленное слово, интернированное в символ, передается в функцию see. Некоторые знаки пунктуации также расцениваются как слова – функ ция punc выводит словесный эквивалент заданного знака пунктуации.
Функция see регистрирует каждое встреченное слово. Ей также необхо димо иметь информацию о предыдущем слове, для чего используется переменная prev. Изначально она содержит псевдослово, соответствую щее точке. Если see уже вызывалась хотя бы один раз, то prev содержит слово, переданное этой функции на предыдущем вызове.
После отработки read-text таблица *words* будет содержать вхождения всех слов в заданном тексте. Их количество можно подсчитать с помо щью hash-table-count. Английские тексты, не отличающиеся особым словесным разнообразием, редко содержат более 10 000 различных слов.
А вот теперь начинается самая веселая часть. На рис. 8.3 представлен код, генерирующий текст с помощью кода на рис. 8.2. Правит балом ре курсивная функция generate-text. Ей необходимо сообщить желаемое количество слов в создаваемом тексте и (необязательно) предыдущее сло во. По умолчанию это точка, и текст начинается с нового предложения.
Для получения каждого последующего слова generate-text вызывает random-text с предыдущим словом. Функция random-text случайным об разом выбирает одно из слов, следующих после prev в исходном тексте. Вероятность выбора одного из перечисленных слов определяется в со ответствии с частотой его появления в тексте.°
Теперь самое время осуществить тестовый запуск нашей программы. Однако вы уже знакомы с примером того, что она может сделать: речь о той самой строфе в начале книги, для генерации которой был исполь зован отрывок из «Paradise Lost» Мильтона.°
1Речь об эпической поэме английского мыслителя Джона Мильтона «Поте рянный рай», изданной в 1667 году. Поэма написана белым стихом. – Прим. перев.
