- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
158 |
Глава 9. Числа |
> (list (minusp -0.0) (zerop -0.0)) (NIL T)
и соответствует zerop, а не minusp.
Предикаты oddp и evenp применимы лишь к целочисленным аргумен там. Первый истинен для нечетных чисел, второй – для четных.
Из всех предикатов, упомянутых в этом разделе, лишь =, /= и zerop при менимы к комплексным числам.
Наибольший и наименьший аргументы могут быть получены с помо щью функций max и min соответственно, при этом должен быть задан хо тя бы один аргумент:
> (list (max 1 2 3 4 5) (min 1 2 3 4 5)) (5 1)
Если среди аргументов есть хотя бы одно число с плавающей запятой, то тип возвращаемого значения зависит от используемой реализации.
9.4. Арифметика
Сложение и вычитание выполняются с помощью + и -. Обе функции мо гут работать с любым количеством аргументов, а в случае их отсутствия возвращают 0. Выражение вида (- n) возвращает –n. Выражение вида
(- x y z)
эквивалентно
(- (- x y) z)
Кроме того, имеются функции 1+ и 1-, которые возвращают свой аргу мент, увеличенный или уменьшенный на 1 соответственно. Имя функ ции 1- может сбить с толку, потому что (1- x) возвращает x - 1, а не 1 - x.
Макросы incf и decf соответственно увеличивают и уменьшают свой аргумент. Выражение вида (incf x n) имеет такой же эффект, как и (setf x (+ x n)), а (decf x n) – как (setf x (- x n)). В обоих случаях второй аргу мент не обязателен и по умолчанию равен 1.
Умножение выполняется с помощью функции *, которая принимает любое количество аргументов и возвращает их общее произведение. Бу дучи вызванной без аргументов, она возвращает 1.
Функция деления / требует задания хотя бы одного аргумента. Вызов (/ n) эквивалентен вызову (/ 1 n):
> (/ 3) 1/3
Вто время как выражение:
(/ x y z)
эквивалентно
(/ (/ x y) z)
9.5. Возведение в степень |
159 |
Обратите внимание на сходное поведение – и / в этом отношении.
Вызванная с двумя целыми аргументами, / возвращает рациональную дробь, если первый аргумент не делится на второй:
> (/ 365 12) 365/12
Если вы будете пытаться вычислять, к примеру, усредненную длину ме сяца, то вам может показаться, что toplevel над вами издевается. В по добных случаях, когда вам действительно нужна десятичная дробь, пользуйтесь функцией float:
> (float 365/12) 30.416666
9.5. Возведение в степень
Чтобы найти xn, вызовем (expt x n):
>(expt 2 5)
32
Ачтобы вычислить lognx, вызовем (log x n):
>(log 32 2)
5.0
Обычно при вычислении логарифма возвращается число с плавающей запятой.
Для нахождения степени числа e существует специальная функция exp:
> (exp 2) 7.389056
Вычисление натурального логарифма выполняется функцией log, кото рой достаточно сообщить лишь само число:
> (log 7.389056) 2.0
Вычисление корней может выполняться с помощью expt, для чего ее второй аргумент должен быть рациональной дробью:
> (expt 27 1/3) 3.0
Но для вычисления квадратных корней быстрее работает функция sqrt:
> (sqrt 4) 2.0
9.6. Тригонометрические функции
Представлением числа π с плавающей запятой является константа pi. Ее точность зависит от используемой реализации. Функции sin, cos и tan
160 |
|
Глава 9. Числа |
вычисляют соответственно синус, косинус и тангенс заданного в радиа |
||
нах угла: |
|
|
> (let ((x (/ pi 4))) |
|
|
(list (sin x) (cos x) (tan x))) |
|
|
(0.7071067811865475d0 |
0.707167811865476d0 |
1.0d0) |
Все вышеперечисленные функции умеют работать с комплексными ар гументами.
Обратные преобразования выполняются функциями asin, acos, atan. Для аргументов, находящихся на отрезке от -1 до 1, asin и acos возвра щают действительные значения.
Гиперболические синус, косинус и тангенс вычисляются через sinh, cosh и tanh соответственно. Для них также определены обратные преоб разования: asinh, acosh, atanh.
9.7. Представление
Common Lisp не накладывает каких-либо ограничений на размер целых чисел. Целые числа небольшого размера, которые помещаются в ма шинном слове, относятся к типу fixnum. Если для хранения числа тре буется памяти больше, чем слово, Лисп переключается на представление bignum, для которого выделяется несколько слов для хранения целого числа. Это означает, что сам язык не накладывает каких-либо ограниче ний на размер чисел, и он зависит лишь от доступного объема памяти.
Константы most-positive-fixnum и most-negative-fixnum задают максималь ные величины, которые могут обрабатываться реализацией без исполь зования типа bignum. Во многих реализациях они имеют следующие значения:
> (values most-positive-fixnum most-negative-fixnum) 536870911 -536870912
Принадлежность к определенному типу проверяется с помощью преди ката typep:
>(typep 1 ’fixnum)
T
>(typep (1+ most-positive-fixnum) ’bignum)
T
Вкаждой реализации имеются свои ограничения на размер чисел с пла вающей запятой. Common Lisp предоставляет четыре типа чисел с пла вающей запятой: short-float, single-float, double-float и long-float. Реа лизации стандарта не обязаны использовать различные представления для разных типов (и лишь некоторые это делают) .
Суть типа short-float в том, что он занимает одно машинное слово. Ти пы single-float и double-float занимают столько места, сколько нужно,
9.8. Пример: трассировка лучей |
161 |
чтобы удовлетворять установленным требованиям к числам одинарной и двойной точности соответственно. Числа типа long-float могут быть большими настолько, насколько это требуется. Но в конкретной реали зации все 4 типа могут иметь одинаковое внутреннее представление.
Тип числа можно задать принудительно, используя соответствующие буквы: s, f, d или l, а также e для экспоненциальной формы. (Заглавные буквы также допускаются, и этим стоит воспользоваться для представ ления типа long-float, потому что малую l легко спутать с цифрой 1.) Наибольшее представление числа 1.0 можно задать как 1L0.
Глобальные ограничения на размер чисел разных типов задаются шест надцатью константами. Их имена выглядят как m-s-f, где m может быть most или least, s соответствует positive или negative, а f – один из четы рех типов чисел с плавающей запятой.°
Превышение заданных ограничений приводит к возникновению ошиб ки в Common Lisp:
> (* most-positive-long-float 10) Error: floating-point-overflow.
9.8. Пример: трассировка лучей
В качестве примера программы, построенной на численных расчетах, в этом разделе приводится решение задачи трассировки лучей. Трасси ровка лучей – это превосходный алгоритм рендеринга изображений, с помощью которого можно получать реалистичные картины. Однако данный метод является довольно дорогостоящим.
Для моделирования трехмерного изображения нам необходимо опреде лить как минимум четыре предмета: наблюдателя, один или более ис точников света, набор объектов моделируемого мира и плоскость ри сунка (image plane), которая служит окном в этот мир. Наша задача – сгенерировать изображение, соответствующее проекции мира на об ласть плоскости рисунка.
Что же делает необычным метод трассировки лучей? Попиксельная от рисовка всей картины и симуляция прохождения луча через виртуаль ный мир. Такой подход позволяет достичь реалистичных оптических эффектов: прозрачности, отражений, затенений; он позволяет задавать отрисовываемый мир как набор геометрических тел, вместо того чтобы строить их из полигонов. Отсюда вытекает относительная прямолиней ность в реализации метода.
На рис. 9.2 показаны основные математические утилиты, которыми мы будем пользоваться. Первая, sq, вычисляет квадрат аргумента. Вторая, mag, возвращает длину вектора по трем его компонентам x, y и z. Эта функция используется в следующих двух. unit-vector возвращает три значения, соответствующие координатам единичного вектора, имею щего то же направление, что и заданный:
162 |
Глава 9. Числа |
> (multiple-value-call #’mag (unit-vector 23 12 47)) 1.0
Кроме того, mag используется в функции distance, вычисляющей рас стояние между двумя точками в трехмерном пространстве. (Определе ние структуры point содержит параметр :conc-name, равный nil. Это зна чит, что функции доступа к соответствующим полям структуры будут иметь такие же имена, как и сами поля, например x вместо point-x.)
(defun sq (x) (* x x))
(defun mag (x y z)
(sqrt (+ (sq x) (sq y) (sq z))))
(defun unit-vector (x y z) (let ((d (mag x y z)))
(values (/ x d) (/ y d) (/ z d))))
(defstruct (point (:conc-name nil)) x y z)
(defun distance (p1 p2) (mag (- (x p1) (x p2))
(- (y p1) (y p2)) (- (z p1) (z p2))))
(defun minroot (a b c) (if (zerop a)
(/ (- c) b)
(let ((disc (- (sq b) (* 4 a c)))) (unless (minusp disc)
(let ((discrt (sqrt disc)))
(min (/ (+ (- b) discrt) (* 2 a))
(/ (- (- b) discrt) (* 2 a))))))))
Рис. 9.2. Математические утилиты
Наконец, функция minroot принимает три действительных числа a, b и c и возвращает наименьшее x, для которого ax2 + bx + c=0. В случае когда a не равно нулю, корни этого уравнения могут быть получены по хоро шо известной формуле:
Код, реализующий функционально ограниченный трассировщик лу чей, представлен на рис. 9.3. Он генерирует черно-белые изображения, освещаемые одним источником света, расположенным там же, где и глаз наблюдателя. (Таким образом достигается эффект фотографии со вспышкой.)
9.8. Пример: трассировка лучей |
163 |
|
|
|
|
|
(defstruct surface color) |
|
|
(defparameter *world* nil) |
|
|
(defconstant eye (make-point :x 0 :y 0 :z 200)) |
|
|
(defun tracer (pathname &optional (res 1)) |
|
|
(with-open-file (p pathname :direction :output) |
|
|
(format p "P2 ~A ~A 255" (* res 100) (* res 100)) |
|
|
(let ((inc (/ res))) |
|
|
(do ((y -50 (+ y inc))) |
|
|
((< (- 50 y) inc)) |
|
|
(do ((x -50 (+ x inc))) |
|
|
((< (- 50 x) inc)) |
|
|
(print (color-at x y) p)))))) |
|
|
(defun color-at (x y) |
|
|
(multiple-value-bind (xr yr zr) |
|
|
(unit-vector (- x (x eye)) |
|
|
(- y (y eye)) |
|
|
(- 0 (z eye))) |
|
|
(round (* (sendray eye xr yr zr) 255)))) |
|
|
(defun sendray (pt xr yr zr) |
|
|
(multiple-value-bind (s int) (first-hit pt xr yr zr) |
|
|
(if s |
|
|
(* (lambert s int xr yr zr) (surface-color s)) |
|
|
0))) |
|
|
(defun first-hit (pt xr yr zr) |
|
|
(let (surface hit dist) |
|
|
(dolist (s *world*) |
|
|
(let ((h (intersect s pt xr yr zr))) |
|
|
(when h |
|
|
(let ((d (distance h pt))) |
|
|
(when (or (null dist) (< d dist)) |
|
|
(setf surface s hit h dist d)))))) |
|
|
(values surface hit))) |
|
|
(defun lambert (s int xr yr zr) |
|
|
(multiple-value-bind (xn yn zn) (normal s int) |
|
|
(max 0 (+ (* xr xn) (* yr yn) (* zr zn))))) |
|
|
|
|
Рис. 9.3. Трассировка лучей
Для представления объектов в виртуальном мире используется струк тура surface. Говоря точнее, она будет включена в структуры, представ ляющие конкретные разновидности объектов, например сферы. Сама структура surface содержит лишь одно поле color (цвет) в интервале от 0 (черный) до 1 (белый) .
Плоскость рисунка располагается вдоль осей x и y. Глаз наблюдателя смотрит вдоль оси z и находится на расстоянии 200 единиц от рисунка.
164 |
Глава 9. Числа |
Чтобы добавить объект, необходимо поместить его в список *world* (из начально nil). Чтобы он был видимым, его z-координата должна быть отрицательной. Рисунок 9.4 демонстрирует прохождение лучей через поверхность рисунка и их падение на сферу.
y
z
глаз наблюдателя плоскость рисунка
x
Рис. 9.4. Трассировка лучей
Функция tracer записывает изображение в файл по заданному пути. Запись производится в обычном ASCII-формате, называемом PGM. По умолчанию размер изображения – 100×100. Заголовок PGM-файла на чинается с тега P2, за которым следуют числа, соответствующие шири не (100) и высоте (100) в пикселах, причем максимально возможное зна чение равно 255. Оставшаяся часть файла содержит 10 000 целых чисел от 0 (черный) до 255 (белый), которые вместе дают 100 горизонтальных полос по 100 пикселов в каждой.
Разрешение изображения может быть изменено с помощью res. Если res равно, например, 2, то изображение будет содержать 200×200 пикселов.
По умолчанию изображение – это квадрат 100×100 на плоскости рисун ка. Каждый пиксел представляет собой количество света, проходящего через данную точку к глазу наблюдателя. Для нахождения этой вели чины tracer вызывает color-at. Эта функция ищет вектор от глаза на блюдателя к заданной точке, затем вызывает sendray, направляя луч вдоль этого вектора через моделируемый мир. В результате sendray по лучает некоторое число от 0 до 1, которое затем масштабируется на от резок от 0 до 255.
Для определения интенсивности света sendray ищет объект, от которого он был отражен. Для этого вызывается функция first-hit, которая нахо дит среди всех объектов в *world* тот, на который луч падает первым, или же убеждается в том, что луч не падает ни на один объект. В послед нем случае возвращается цвет фона, которым мы условились считать 0 (черный) . Если луч падает на один из объектов, то нам необходимо найти долю света, отраженного от него. Согласно закону Ламберта, интенсив ность света, отраженного точкой поверхности, пропорциональна ска лярному произведению единичного вектора N, направленного из этой
9.8. Пример: трассировка лучей |
165 |
точки вдоль нормали к поверхности (вектор, длина которого равна 1, направленный перпендикулярно поверхности в заданной точке), и еди ничного вектора L, направленного вдоль луча к источнику света:
i = N L
Если источник света находится в этой точке, N и L будут иметь одина ковое направление, и их скалярное произведение будет иметь макси мальное значение, равное 1. Если поверхность находится под углом 90° к источнику света, то N и L будут перпендикулярны и их произведение будет равно 0. Если источник света находится за поверхностью, то про изведение будет отрицательным.
Внашей программе мы предполагаем, что источник света находится там же, где и глаз наблюдателя, поэтому функция lambert, использую щая вышеописанное правило для нахождения освещенности некото рой точки поверхности, возвращает скалярное произведение нормали и трассируемого луча.
Вфункции sendray это число умножается на цвет поверхности (темная поверхность отражает меньше света), чтобы определить интенсивность в заданной точке. Упростим задачу, ограничившись лишь объектами типа сферы. Код, реализующий поддержку сфер, приведен на рис. 9.5. Структура sphere включает surface, поэтому sphere будет иметь пара метр color, так же как и параметры center и radius. Вызов defsphere до бавляет в наш мир новую сферу.
Функция intersect (пересечение) распознает тип объекта и вызывает со ответствующую ему функцию. На данный момент мы умеем работать лишь со сферами. Для сферы из intersect будет вызываться sphere-in tersect, но наш код может быть с легкостью расширен для поддержки других объектов.
Как найти пересечение луча со сферой? Луч представляется точкой p = (x0, y0, z0) и единичным вектором v = (xr, yr, zr). Любая точка луча мо жет быть выражена как p + nv для некоторого n или, что то же самое, (x0+nxr, y0+nyr, z0+nzr). Когда луч падает на сферу, расстояние до центра (xc, yc, zc) равно радиусу сферы r. Таким образом, условие пересечения луча и сферы можно записать следующим образом:
Из этого следует, что
где
166 |
Глава 9. Числа |
(defstruct (sphere (:include surface)) radius center)
(defun defsphere (x y z r c) (let ((s (make-sphere
:radius r
:center (make-point :x x :y y :z z) :color c)))
(push s *world*) s))
(defun intersect (s pt xr yr zr)
(funcall (typecase s (sphere #’sphere-intersect)) s pt xr yr zr))
(defun sphere-intersect (s pt xr yr zr) (let* ((c (sphere-center s))
(n (minroot (+ (sq xr) (sq yr) (sq zr))
(* 2 (+ (* (- (x pt) (x c)) xr) (* (- (y pt) (y c)) yr) (* (- (z pt) (z c)) zr)))
(+ (sq (- (x pt) (x c))) (sq (- (y pt) (y c))) (sq (- (z pt) (z c)))
(- (sq (sphere-radius s)))))))
(if n
(make-point :x (+ (x pt) (* n xr)) :y (+ (y pt) (* n yr))
:z (+ (z pt) (* n zr))))))
(defun normal (s pt)
(funcall (typecase s (sphere #’sphere-normal)) s pt))
(defun sphere-normal (s pt) (let ((c (sphere-center s)))
(unit-vector (- (x c) (x pt)) (- (y c) (y pt))
(- (z c) (z pt)))))
Рис. 9.5. Сферы
Для нахождения точки пересечения нам необходимо решить это квад ратное уравнение. Оно может иметь ноль, один или два вещественных корня. Отсутствие решений означает, что луч проходит мимо сферы; одно решение означает, что луч пересекает сферу лишь в одной точке (то есть касается ее); два решения сигнализируют о том, что луч прохо дит через сферу, пересекая ее поверхность два раза. В последнем случае нам интересен лишь наименьший из корней. При удалении луча от гла за наблюдателя n увеличивается, поэтому наименьшее решение будет
9.8. Пример: трассировка лучей |
167 |
соответствовать меньшему n. Для нахождения корня используется min root. Если корень существует, sphere-intersect возвратит соответствую щую ему точку (x0 + nxr, y0 + nyr, z0 + nzr).
Две другие функции на рис. 9.5, normal и sphere-normal, связаны между собой аналогично intersect и sphere-intersect. Поиск нормали для сфе ры крайне прост – это всего лишь вектор, направленный из точки на поверхности к ее центру.
На рис. 9.6 показано, как будет выполняться генерация изображения; ray-trace создает 38 сфер (не все они будут видимыми), а затем генериру ет изображение, которое записывается в файл "spheres.pgm". Итог рабо ты программы с параметром res = 10 представлен на рис. 9.7.
(defun ray-test (&optional (res 1)) (setf *world* nil)
(defsphere 0 -300 -1200 200 .8) (defsphere -80 -150 -1200 200 .7) (defsphere 70 -100 -1200 200 .9) (do ((x -2 (1+ x)))
((> x 2))
(do ((z 2 (1+ z))) ((> z 7))
(defsphere (* x 200) 300 (* z -400) 40 .75))) (tracer (make-pathname :name "spheres.pgm") res))
Рис. 9.6. Использование трассировщика
Рис. 9.7. Изображение, полученное методом трассировки лучей