- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
38 |
Глава 2. Добро пожаловать в Лисп |
Глобальная переменная может быть создана с помощью оператора def parameter:
> (defparameter *glob* 99) *GLOB*
Такая переменная будет доступна везде, кроме выражений, в которых создается локальная переменная с таким же именем. Чтобы избегать та ких случайных совпадений, принято давать глобальным переменным имена, окруженные звездочками.
Также в глобальном окружении можно задавать константы, используя оператор defconstant1:
(defconstant limit (+ *glob* 1))
Нет необходимости давать константам имена, окруженные звездочка ми, потому что любая попытка определить переменную с таким же име нем закончится ошибкой. Чтобы узнать, соответствует ли какое-то имя глобальной переменной или константе, воспользуйтесь boundp:
>(boundp ’*glob*)
T
2.11.Присваивание
ВCommon Lisp наиболее общим средством присваивания значений яв ляется оператор setf. Он может присваивать значения переменным лю бых типов:
>(setf *glob* 98)
98
> (let ((n 10)) (setf n 2) n)
2
Если setf пытается присвоить значение переменной, которая в данном контексте не является локальной, то переменная будет определена как глобальная:
> (setf x (list ’a ’b ’c)) (A B C)
Таким образом, вы можете создавать глобальные переменные неявно, просто присваивая им значения. Однако в файлах исходного кода счи тается хорошим тоном задавать их явно с помощью оператора defpara meter.
1Как и для специальных переменных, для определяемых пользователем кон стант существует негласное правило, согласно которому их имена следует окружать знаками +. Следуя этому правилу, в примере выше мы бы опреде лили константу +limit+. – Прим. перев.
2.12. Функциональное программирование |
39 |
Но можно делать больше, чем просто присваивать значения перемен ным. Первым аргументом setf может быть не только переменная, но и выражение. В таком случае значение второго аргумента вставляется в то место, на которое ссылается это выражение:
>(setf (car x) ’n)
N
>x
(N B C)
Первым аргументом setf может быть почти любое выражение, которое ссылается на какое-то место. Все такие операторы отмечены в приложе нии D как работающие с setf.
Также setf можно передать любое (четное) число аргументов. Следую щий вызов
(setf a b c d e f)
эквивалентен трем последовательным вызовам setf:
(setf a b) (setf c d) (setf e f)
2.12. Функциональное программирование
Функциональное программирование – это понятие, которое означает написание программ, работающих через возвращаемые значения, а не изменения среды. Это основная парадигма в Лиспе. Большинство встро енных в Лисп функций не вызывают побочных эффектов.
Например, функция remove принимает список и объект и возвращает новый список, состоящий из тех же элементов, что и предыдущий, за исключением этого объекта.
>(setf lst ’(c a r a t)) (C A R A T)
>(remove ’a lst)
(C R T)
Почему бы не сказать, что remove просто удаляет элемент из списка? По тому что она этого не делает. Исходный список остался нетронутым:
> lst
(C A R A T)
А что если вы хотите действительно удалить элементы из списка? В Лис пе принято предоставлять список в качестве аргумента какой-либо функ ции и присваивать возвращаемое ей значение с помощью setf. Чтобы уда лить все a из списка x, мы скажем:
(setf x (remove ’a x))
40 |
Глава 2. Добро пожаловать в Лисп |
В функциональном программировании избегают использовать setf и другие подобные вещи. Поначалу сложно вообразить, что это попросту возможно, не говоря уже о том, что желательно. Как можно создавать программы только с помощью возвращения значений?
Конечно, совсем без побочных эффектов работать неудобно. Однако, чи тая дальше эту книгу, вы будете все сильнее удивляться тому, как, в сущности, немного побочных эффектов необходимо. И чем меньше вы будете ими пользоваться, тем лучше.
Одно из главных преимуществ использования функционального про граммирования заключается в интерактивном тестировании. Это означает, что вы можете тестировать функции, написанные в функцио нальном стиле, сразу же после их написания, и при этом вы будете уве рены, что результат выполнения всегда будет одинаковым. Вы можете изменить одну часть программы, и это не повлияет на корректность ра боты другой ее части. Такая возможность безбоязненно изменять код делает доступным новый стиль программирования, подобно тому как телефон в сравнении с письмами предоставляет новый способ общения.
2.13. Итерация
Когда мы хотим повторить что-то многократно, порой удобнее исполь зовать итерацию, чем рекурсию. Типичный пример использования ите рации – генерация таблиц. Следующая функция
(defun show-squares (start end) (do ((i start (+ i 1)))
((> i end) ’done)
(format t "~A ~A~%" i (* i i))))
печатает квадраты целых чисел от start до end:
> (show-squares 2 5)
2 |
4 |
3 |
9 |
4 |
16 |
5 |
25 |
DONE
Макрос do – основной итерационный оператор в Common Lisp. Как и в случае let, первый аргумент do задает переменные. Каждый элемент этого списка может выглядеть как:
(variable initial update)
где variable – символ, а initial и update – выражения. Исходное значе ние каждой переменной определяется значением initial, и на каждой итерации это значение будет меняться в соответствии со значением update. В примере show-squares do использует только одну переменную, i. На первой итерации значение i равно start, и после каждой успешной итерации оно будет увеличиваться на единицу.
2.13. Итерация |
41 |
Второй аргумент do – список, состоящий по меньшей мере из одного вы ражения, которое определяет, когда итерации следует остановиться. В нашем примере этим выражением является проверка (> i end). Ос тальные выражения в списке будут вычислены после завершения ите раций, и значение последнего из них будет возвращено как значение оператора do. Функция show-squares всегда возвращает done.
Оставшиеся аргументы do составляют тело цикла. Они будут вычис ляться по порядку на каждой итерации. На каждом шаге в первую оче редь модифицируются значения переменных, затем следует проверка условия окончания цикла, после которой (если условие не выполнено) вычисляется тело цикла.
Для сравнения запишем рекурсивную версию show-squares:
(defun show-squares (i end) (if (> i end)
’done
(progn
(format t "~A ~A~%" i (* i i)) (show-squares (+ i 1) end))))
В ней мы использовали новый оператор progn. Он принимает любое ко личество выражений, вычисляет их одно за другим и возвращает зна чение последнего.
Для частных случаев в Common Lisp существуют более простые итераци онные операторы. К примеру, запишем функцию, определяющую дли ну списка:
(defun our-length (lst) (let ((len 0))
(dolist (obj lst)
(setf len (+ len 1))) len))
В этом примере оператор dolist принимает первый аргумент вида (vari able expression) и следующий за ним набор выражений. Они вычисля ются для каждого элемента списка, значение которых по очереди при сваивается переменной. В этом примере для каждого obj в lst увеличи вается значение len.
Рекурсивная версия этой функции:
(defun our-length (lst) (if (null lst)
0
(+ (our-length (cdr lst)) 1)))
Если список пуст, его длина равна 0, иначе она на 1 больше длины списка cdr. Эта версия функции our-length более понятна, однако она неэффек тивна, поскольку не использует хвостовую рекурсию (см. раздел 13.2).