- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
5.5. Множественные значения |
103 |
HIP HOP FLIP FLOP SLIP SLOP (HIP FLIP SLIP)
Функция mapc всегда возвращает свой второй аргумент.
5.5. Множественные значения
Чтобы подчеркнуть важность функционального программирования, часто говорят, что в Лиспе каждое выражение возвращает значение. На самом деле, все несколько сложнее. В Common Lisp выражение может возвращать несколько значений или же не возвращать ни одного. Мак симальное их количество может различаться в разных реализациях, но должно быть не менее 19.
Для функции, которая вычисляет несколько значений, эта возможность позволяет возвращать их без заключения в какую-либо структуру. На пример, встроенная функция get-decoded-time возвращает текущее вре мя в виде девяти значений: секунды, минуты, часы, день, месяц, год и еще два.
Множественные значения также позволяют функциям поиска разде лять случаи, когда элемент не найден и когда найден элемент nil. Имен но поэтому gethash возвращает два значения. Второе значение инфор мирует об успешности поиска, поэтому мы можем хранить nil в хештаблице точно так же, как и остальные значения.
Множественные значения можно возвратить с помощью функции va lues. Она возвращает в точности те значения, которые были переданы ей в виде аргументов:
> (values ’a nil (+ 2 4)) A
NIL 6
Если вызов values является последним в теле функции, то эта функция возвращает те же множественные значения, что и values. Множествен ные значения могут передаваться через несколько вызовов:
> ((lambda () ((lambda () (values 1 2))))) 1 2
Однако если в цепочке передачи значений что-либо принимает лишь один аргумент, то все остальные будут потеряны:
> (let ((x (values 1 2))) x)
1
Используя values без аргументов, мы имеем возможность не возвращать никаких значений. Если же что-либо будет ожидать значение, будет возвращен nil.
104 |
Глава 5. Управление |
>(values)
>(let ((x (values))) x)
NIL
Чтобы получить несколько значений, можно воспользоваться multiple- value-bind:
> (multiple-value-bind (x y z) (values 1 2 3) (list x y z))
(1 2 3)
> (multiple-value-bind (x y z) (values 1 2) (list x y z))
(1 2 NIL)
Если переменных больше, чем возвращаемых значений, лишним пере менным будет присвоено значение nil. Если же значений возвращается больше, чем выделено переменных, то лишние значения будут отброше ны. Таким образом, вы можете написать функцию, которая просто пе чатает текущее время:°
> (multiple-value-bind (s m h) (get-decoded-time) (format nil "~A:~A:~A" h m s))
"4:32:13"
Можно передать какой-либо функции множественные значения в каче стве аргументов с помощью multiple-value-call:
> (multiple-value-call #’+ (values 1 2 3)) 6
Кроме того, есть функция multiple-value-list:
> (multiple-value-list (values ’a ’b ’c)) (A B C)
которая действует так же, как multiple-value-call с функцией #’list
в качестве первого аргумента.
5.6.Прерывание выполнения
Чтобы выйти из блока в любой момент, достаточно вызвать return. Одна ко иногда может потребоваться нечто более радикальное, например пе редача управления через несколько вызовов функций. Для этой цели существуют специальные операторы catch и throw. Конструкция catch использует метку, которой может быть любой объект. За меткой следу ет набор выражений.
(defun super () (catch ’abort (sub)
(format t "We’ll never see this.")))
5.6. Прерывание выполнения |
105 |
(defun sub ()
(throw ’abort 99))
Выражения после метки вычисляются так же, как в progn. Вызов throw внутри любого из этих выражений приведет к немедленному выходу из catch:
> (super) 99
Оператор throw с заданной меткой передаст управление соответствую щей конструкции catch (то есть завершит ее выполнение) через любое количество catch с другими метками (соответственно «убив» их). Если нет ожидающего catch с требуемой меткой, throw вызовет ошибку.
Вызов error также прерывает выполнение, но вместо передачи управле ния вверх по дереву вызовов он передает его обработчику ошибок Лис па. В результате вы, скорее всего, попадете в отладчик (break loop). Вот что произойдет в некой абстрактной реализации Common Lisp:
> (progn
(error "Oops! ")
(format t "After the error. ")) Error: Oops!
Options: :abort, :backtrace
>>
Более подробную информацию об ошибках и условиях можно найти в приложении A.
Иногда вам может понадобиться некая гарантия защиты от ошибок и прерываний типа throw. Используя unwind-protect, вы можете быть уве рены, что в результате подобных явлений программа не окажется в про тиворечивом состоянии. Конструкция unwind-protect принимает любое количество аргументов и возвращает значение первого. Остальные выра жения будут вычислены, даже если вычисление первого будет прервано.
>(setf x 1)
1
>(catch ’abort (unwind-protect
(throw ’abort 99) (setf x 2)))
99 > x 2
Здесь, даже несмотря на то что throw передал управление catch, второе выражение было вычислено прежде, чем был осуществлен выход из catch. В случае когда перед преждевременным завершением необходи ма какая-то очистка или сброс, unwind-protect может быть очень полез на. Один из таких примеров представлен на стр. 295.
106 Глава 5. Управление
5.7. Пример: арифметика над датами
В некоторых приложениях полезно иметь возможность складывать и вычитать даты, например, чтобы сказать, что через 60 дней после 17 де кабря 1997 года настанет 15 февраля 1998. В этом разделе мы создадим необходимые для этого инструменты. Нам потребуется конвертировать даты в целые числа, при этом за ноль будет принята дата 1 января 2000 го да. Затем мы сможем работать с датами как с целыми числами, исполь зуя функции + и –. Кроме того, необходимо уметь конвертировать целое число обратно в дату.
Чтобы преобразовать дату в целое число, будем складывать количества дней, соответствующие различным компонентам даты. Например, чис ло, соответствующее 13 ноября 2004 года, получим, складывая количе ство дней до 2004 года, количество дней в году до ноября и число 13.
Нам потребуется таблица, устанавливающая соответствие между меся цем и количеством дней в нем для невисокосного года. Начнем со спи ска, содержащего длины месяцев:
> (setf mon ’(31 28 31 30 31 30 31 31 30 31 30 31)) (31 28 31 30 31 30 31 31 30 31 30 31)
Проведем несложную проверку, сложив все эти числа:
> (apply #’+ mon) 365
Теперь перевернем этот список и применим с помощью maplist функцию + к последовательности хвостов (cdr) списка. Мы получим количества дней, прошедшие до начала каждого последующего месяца:
> (setf nom (reverse mon))
(31 30 31 30 31 31 30 31 30 31 28 31) > (setf sums (maplist #’(lambda (x)
(apply #’+ x)) nom))
(365 334 304 273 243 212 181 151 90 59 31) > (reverse sums)
(31 59 90 120 151 181 212 243 273 304 334 365)
Эти цифры означают, что до начала февраля пройдет 31 день, до начала марта 59 и т. д.
На рис. 5.1. приведен код, выполняющий преобразования дат. В нем по лученный нами список преобразован в вектор.
Жизненный цикл обычной Лисп-программы состоит из четырех этапов: сначала она пишется, потом читается, компилируется и затем исполня ется. Отличительной особенностью Лиспа является тот факт, что Лиспсистема принимает участие в каждом шаге этой последовательности. Вы можете взаимодействовать с Лиспом не только во время работы про граммы, но также во время компиляции (раздел 10.2) и даже ее чтения
5.7. Пример: арифметика над датами |
107 |
(раздел 14.3). То, каким образом мы создали вектор month, свидетельству ет о том, что мы используем Лисп даже во время написания программы.
Производительность программы имеет значение лишь на четвертом этапе – при ее выполнении. На первых трех этапах вы можете в полной мере пользоваться гибкостью и мощью списков, не задумываясь об их стоимости.
(defconstant month
#(0 31 59 90 120 151 181 212 243 273 304 334 365))
(defconstant yzero 2000)
(defun leap? (y)
(and (zerop (mod y 4))
(or (zerop (mod y 400))
(not (zerop (mod y 100))))))
(defun date->num (d m y)
(+ (- d 1) (month-num m y) (year-num y)))
(defun month-num (m y)
(+ (svref month ( - m 1))
(if (and (> m 2) (leap? y)) 1 0)))
(defun year-num (y) (let ((d 0))
(if (>= y yzero)
(dotimes (i (- y yzero) d)
(incf d (year-days (+ yzero i)))) (dotimes (i (- yzero y) (- d))
(incf d (year-days (+ y i)))))))
(defun year-days (y) (if (leap? y) 366 365))
Рис. 5.1. Операции с датами: преобразование дат в целые числа
Если вы вздумаете использовать этот код для управления машиной вре мени, то люди могут не согласиться с вами по поводу текущей даты, ес ли вы попадете в прошлое. По мере накопления информации о продол жительности года люди вносили изменения в календарь. В англоговоря щих странах подобная нестыковка последний раз устранялась в 1752 го ду, когда сразу после 2 сентября последовало 14 сентября.°
Количество дней в году зависит от того, является ли он високосным. Год не является високосным, если он не кратен 4 либо кратен 100 и не кратен 400. 1904 год был високосным, 1900 не был, а 1600 был.
Для определения делимости существует функция mod, возвращающая остаток от деления первого аргумента на второй:
> (mod 23 5) 3
108 |
Глава 5. Управление |
> (mod 25 5) 0
Одно число считается делимым на другое, если остаток от деления на него равен нулю. Функция leap? использует этот подход для определе ния високосности года:
> (mapcar #’leap? ’(1904 1900 1600)) (T NIL T)
Дату в целое число преобразует функция date->num. Она возвращает сумму численных представлений каждого компонента даты. Чтобы вы яснить, сколько дней прошло до начала заданного месяца, она исполь зует функцию month-num, которая обращается к соответствующему ком поненту вектора month, добавляя к нему 1, если год високосный и задан ный месяц находится после февраля.
Чтобы найти количество дней до наступления заданного года, date->num вызывает year-num, которая возвращает численное представление 1 ян варя этого года. Эта функция отсчитывает годы до нулевого года (то есть 2000).
На рис. 5.2 показан код второй части программы. Функция num->date преобразует целые числа обратно в даты. Вызывая num-year, она получа ет год и количество дней в остатке, по которому num-month вычисляет месяц и день.
Как и year-num, num-year ведет отсчет от 2000 года, складывая продолжи тельности каждого года до тех пор, пока эта сумма не превысит заданное число n (или не будет равна ему). Если сумма, полученная на какой-то итерации, превысит его, то num-year возвращает год, полученный на пре дыдущей итерации. Для этого введена переменная prev, которая хранит число дней, полученное на предыдущей итерации.
Функция num-month и ее подпроцедура nmon ведут себя обратно month-num. Они вычисляют позицию в векторе month по численному значению, то гда как month-num вычисляет значение исходя из заданного элемента вектора.
Первые две функции на рис. 5.2 могут быть объединены в одну. Вместо вызова отдельной функции num-year могла бы вызывать непосредствен но num-month. Однако на этапе тестирования и отладки текущий вариант более удобен, и объединение функций может стать следующим шагом после тестирования.
Функции date->num и num->date упрощают арифметику дат.° С их помо щью работает функция date+, позволяющая складывать и вычитать дни из заданной даты. Теперь мы сможем вычислить дату по прошествии 60 дней с 17 декабря 1997 года:
> (multiple-value-list (date+ 17 12 1997 60)) (15 2 1998)
Мы получили 15 февраля 1998 года.