- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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.3. Условные выражения |
99 |
5.3. Условные выражения
Наиболее простым условным оператором можно считать if. Все осталь ные являются его расширениями. Оператор when является упрощением if. Его тело, которое может состоять из нескольких выражений, вычис ляется лишь в случае истинности тестового выражения. Таким образом,
(when (oddp that)
(format t "Hmm, that’s odd. ") (+ that 1))
эквивалентно
(if (oddp that) (progn
(format t "Hmm, that’s odd. ") (+ that 1)))
Оператор unless действует противоположно when. Его тело вычисляется лишь в случае ложности тестового выражения.
Матерью всех условных выражений (в обоих смыслах) является cond, ко торый предлагает два новых преимущества: неограниченное количест во условных переходов и неявное использование progn в каждом из них. Его имеет смысл использовать в случае, когда третий аргумент if – дру гое условное выражение. Например, следующее определение our-member:
(defun our-member (obj lst) (if (atom lst)
nil
(if (eql (car lst) obj) lst
(our-member obj (cdr lst)))))
может быть переписано с помощью cond:
(defun our-member (obj lst) (cond ((atom lst) nil)
((eql (car lst) obj) lst)
(t (our-member obj (cdr lst)))))
Вдействительности, реализация Common Lisp, вероятно, оттранслиру ет второй вариант в первый.
Вобщем случае cond может принимать ноль или более аргументов. Каж дый аргумент должен быть представлен списком, состоящим из усло вия и следующих за ним выражений, которые будут вычисляться в слу чае истинности этого условия. При вычислении вызова cond условия проверяются по очереди до тех пор, пока одно из них не окажется истин ным. Далее вычисляются выражения, следующие за этим условием, и возвращается значение последнего из них. Если после выражения, оказавшегося истинным, нет других выражений, то возвращается его значение:
100 |
Глава 5. Управление |
> (cond (99)) 99
Если условие имеет тестовое выражение t, оно будет истинным всегда. Этот факт удобно использовать для указания условия, которое будет выполняться в случае, когда все остальные не подошли. По умолчанию в таком случае возвращается nil, но считается плохим стилем исполь зовать этот вариант. (Подобная проблема показана на стр. 132.)
Если вы сравниваете выражение с набором постоянных, уже вычислен ных объектов, правильнее будет использовать конструкцию case. На пример, мы можем воспользоваться case для преобразования имени ме сяца в его продолжительность в днях:
(defun month-length (mon) (case mon
((jan mar may jul aug oct dec) 31) ((apr jun sept nov) 30)
(feb (if (leap-year) 29 28)) (otherwise "unknown month")))
Конструкция case начинается с выражения, значение которого будет сравниваться с предложенными вариантами. За ним следуют выраже ния, начинающиеся либо с ключа, либо со списка ключей, за которым следует произвольное количество выражений. Сами ключи рассматри ваются как константы и не вычисляются. Значение ключа (ключей) сравнивается с оригинальным объектом с помощью eql. В случае совпа дения вычисляются следующие за ключом выражения. Результатом вызова case является значение последнего вычисленного выражения.
Вариант по умолчанию может быть обозначен через t или otherwise. Ес ли ни один из ключей не подошел или вариант, содержащий подходя щий ключ, не содержит выражений, возвращается nil:
> (case 99 (99)) NIL
Макрос typecase похож на case, но вместо ключей использует специфи каторы типов, а искомое выражение сверяется с ними через typep вме сто eql. (Пример с использованием typecase приведен на стр. 118.)
5.4. Итерации
Основной итерационный оператор, do, был представлен в разделе 2.13. Поскольку do неявным образом использует block и tagbody, внутри него можно использовать return, return-from и go.
В разделе 2.13 было указано, что первый аргумент do должен быть спи ском, содержащим описания переменных вида
(variable initial update)
5.4. Итерации |
101 |
Выражения initial и update не обязательны. Если выражение update пропущено, значение соответствующей переменной не будет меняться. Если пропущено initial, то переменной присваивается первоначальное значение nil.
В примере на стр. 40:
(defun show-squares (start end) (do ((i start (+ i 1)))
((> i end) ’done)
(format t "~A ~A~%" i (* i i))))
для переменной, создаваемой do, задано выражение update. Практиче ски всегда в процессе выполнения выражения do изменяется хотя бы одна переменная.
В случае модификации сразу нескольких переменных встает вопрос: что произойдет, если форма обновления одной из них ссылается на дру гую переменную, которая также должна обновиться? Какое значение последней переменной будет использоваться: зафиксированное на пре дыдущей итерации или уже измененное? Макрос do использует значе ние предыдущей итерации:
> (let ((x ’a))
(do ((x 1 (+ x 1)) (y x x))
((> x 5))
(format t "(~A ~A) " x y))) (1 A) (2 1) (3 2) (4 3) (5 4)
NIL
На каждой итерации значение x увеличивается на 1, а y получает значе ние x из предыдущей итерации.
Существует также оператор do*, относящийся к do, как let* к let. В нем начальные (initial) или последующие (update) выражения для вычисле ния переменных могут ссылаться на предыдущие переменные, и при этом они будут получать их текущие значения:
> (do* ((x 1 (+ x 1)) (y x x))
((> x 5))
(format t "(~A ~A) " x y)) (1 1) (2 2) (3 3) (4 4) (5 5) NIL
Помимо do и do* существуют и более специализированные операторы. Итерация по списку выполняется с помощью dolist:
> (dolist (x ’(a b c d) ’done) (format t "~A " x))
A B C D DONE
102 |
Глава 5. Управление |
Суть оператора do
В книге «The Evolution of Lisp» Стил и Гэбриэл выражают суть do настолько точно, что соответствующий отрывок из нее до стоин цитирования:
Отложим обсуждение синтаксиса в сторону. Нужно признать, что цикл, выполняющий лишь итерацию по одной переменной, до вольно бесполезен в любом языке программирования. Практиче ски всегда в таких случаях одна переменная используется для получения последовательных значений, в то время как еще одна собирает их вместе. Если цикл лишь увеличивает значение пере менной на заданную величину, то каждое новое значение должно устанавливаться благодаря присваиванию… или иным побочным эффектам. Цикл do с несколькими переменными выражает внут реннюю симметрию генерации и аккумуляции, позволяя выра зить итерацию без явных побочных эффектов:
(defun factorial (n) (do ((j n (- j 1))
(f 1 (* j f))) ((= j 0) f)))
И вправду, нет ничего необычного в циклах do с пустым телом, которые выполняют всю работу в самих шагах итерации.°
Третье выражение в начальном списке будет вычислено на выходе из цикла. По умолчанию это nil.
Похожим образом действует dotimes, пробегающий для заданного числа n значения от 0 до n–1:
> (dotimes (x 5 x) (format t "~A " x))
0 1 2 3 4
5
Как и в dolist, третье выражение в начальном списке не обязательно и по умолчанию установлено как nil. Обратите внимание, что это выра жение может содержать саму итерируемую переменную.
Функция mapc похожа на mapcar, однако она не строит список из вычис ленных значений, поэтому может использоваться лишь ради побочных эффектов. Она более гибка, чем dolist, потому что может выполнять итерацию параллельно сразу по нескольким спискам:
> (mapc #’(lambda (x y)
(format t "~A ~A " x y)) ’(hip flip slip)
’(hop flop slop))
