- •Оглавление
- •Предисловие
- •Предисловие к русскому изданию
- •Глава 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. Анализ
- •Комментарии
- •Алфавитный указатель
134 |
Глава 7. Ввод и вывод |
Чтобы избежать подобных случаев, не стоит применять read напрямую. Лучший способ: пользовательский ввод читается с помощью read-line и далее обрабатывается с помощью read-from-string.° Эта функция при нимает строку и возвращает первый объект, прочитанный из нее:
> (read-from-string "a b c") A
2
Помимо прочитанного объекта она возвращает позицию в строке, на ко торой завершилось чтение.
В общем случае read-from-string может принимать два необязательных аргумента и три аргумента по ключу. Два необязательных – те же, что и третий и четвертый аргументы в read-line (вызывать ли ошибку при достижении конца строки и, если нет, что возвращать в этом случае) . А аргументы по ключу :start и :end ограничивают часть строки, в кото рой будет выполняться чтение.
Рассмотренные в этом разделе функции определены с помощью более примитивной read-char, считывающей одиночный знак. Она принимает те же четыре необязательных аргумента, что и read, и read-line. Common Lisp также определяет функцию peek-char, которая похожа на read-char, но не удаляет прочитанный знак из потока.
7.3. Вывод
Имеются три простейшие функции вывода: prin1, princ и terpri. Всем им можно сообщить выходной поток; по умолчанию это *standard-output* .
Разница между prin1 и princ, грубо говоря, в том, что prin1 генерирует вывод для программ, а princ – для людей. Так, например, prin1 печатает двойные кавычки вокруг строк, а princ – нет:
>(prin1 "Hello") "Hello"
"Hello"
>(princ "Hello") Hello
"Hello"
Обе функции возвращают свой первый аргумент, который, кстати, сов падает с тем, который отображает prin1. Функция terpri печатает толь ко новую строку.
Зная об этих функциях, вам легче будет понять поведение более общей format. С помощью функции format может быть осуществлен практиче ски любой вывод. Она принимает поток (а также t или nil), строку фор матирования и ноль или более аргументов. Строка форматирования мо жет содержать директивы форматирования, которые начинаются со знака ~ (тильда). На их место будут выведены представления аргумен тов, переданных format.
7.3. Вывод |
135 |
Передавая t в качестве первого аргумента, мы направляем вывод в *stan dard-output*. Если первый аргумент – nil, то format возвратит строку вме сто того, чтобы ее напечатать. Ради краткости будем приводить только такие примеры. Функцию format можно рассматривать одновременно и как невероятно мощный, и как жутко сложный инструмент. Она по нимает огромное количество директив, но только часть из них вам при дется использовать. Две наиболее распространенные директивы: ~A и ~%. (Между ~a и ~A нет различия, однако принято использовать вторую ди рективу, потому что в строке форматирования она более заметна.) ~A – это место для вставки значения, которое будет напечатано с помощью princ. ~% соответствует новой строке.
> (format nil "Dear ~A,~% Our records indicate…" "Mr. Malatesta")
"Dear Mr. Malatesta, Our records indicate…"
В этом примере format возвращает один аргумент – строку, содержащую символ переноса строки.
Директива ~S похожа на ~A, однако она выводит объекты так же, как prin1, а не как princ:
> (format t "~S ~A" "z" "z") "z" z
NIL
Директивы форматирования сами могут принимать аргументы. ~F ис пользуется для печати выровненных справа чисел с плавающей запя той1 и может принимать пять аргументов:
1.Суммарное количество выводимых символов. По умолчанию это дли на числа.
2.Количество выводимых символов после точки. По умолчанию выво дятся все.
3.Смещение точки влево (смещение на один знак соответствует умно жению на 10). По умолчанию отсутствует.
4.Символ, который будет выведен вместо числа, если оно не умещает ся в количество символов, разрешенное первым аргументом. Если ничего не задано, то число, превышающее допустимый лимит, будет напечатано как есть.
5.Символ, печатаемый перед левой цифрой. По умолчанию – пробел.
1В англоязычной литературе принято использовать точку в качестве разде лителя в десятичных дробях. В русском языке принято использовать запя тую, а такие числа называть «числами с плавающей запятой» вместо «flo ating point numbers» (числа с плавающей точкой). В Common Lisp для запи си десятичных дробей всегда используется точка. – Прим. перев.
136 |
Глава 7. Ввод и вывод |
Вот пример, в котором используются все пять аргументов:
> (format nil "~10,2,0,’*,’ F" 26.21875)
"26.22"
Исходное число округляется до двух знаков после точки (сама точка смещается на 0 положений влево, то есть не смещается), для печати чис ла выделяется пространство в 10 символов, на месте незаполненных сле ва полей печатаются пробелы. Обратите внимание, что символ звездоч ки передается как ’*, а не как обычно #\*. Поскольку заданное число вписывается в предложенное пространство 10 символов, четвертый ар гумент не используется.
Все эти аргументы не обязательны для использования. Чтобы приме нить значение по умолчанию, достаточно просто пропустить соответст вующий аргумент. При желании напечатать число, округленное до двух знаков после точки, достаточно написать:
> (format nil "~,2,,,F" 26.21875) "26.22"
Такая последовательность запятых может быть опущена, поэтому бо лее распространена следующая запись:
> (format nil "~,2F" 26.21875) "26.22"
Предупреждение: Округляя числа, format не гарантирует округление в большую или меньшую сторону. Поэтому (format nil "~,1F" 1.25) мо жет выдать либо "1.2", либо "1.3". Таким образом, если вам нужно округ ление в конкретную сторону (например, при конвертации валют), перед печатью округляйте выводимое число явным образом.
7.4. Пример: замена строк
В этом разделе приведен пример использования ввода-вывода – простая программа для замены строк в текстовых файлах. Мы создадим функ цию, которая сможет заменить каждую строку old в файле на строку new. Простейший способ сделать это – сверять каждый символ с первым символом old. Если они не совпадают, то мы можем просто напечатать символ из файла. Если они совпадают, то сверяем следующий символ из файла со вторым символом old, и т. д. Если найдено совпадение, то печатаем на выход строку new.°
Что происходит, когда мы натыкаемся на несовпадение в процессе свер ки символов? Например, предположим, что мы ищем шаблон "abac", а входной файл содержит "ababac". Совпадение будет наблюдаться вплоть до четвертого символа: в файле это b, а в шаблоне c. Дойдя до четвертого символа, мы понимаем, что можем напечатать первый символ a. Однако некоторые пройденные символы нам все еще нужны, потому что третий прочтенный символ a совпадает с первым символом шаблона. Таким
7.4. Пример: замена строк |
137 |
образом, нам понадобится место, где мы будем хранить все прочитан ные символы, которые нам пока еще нужны.
Очередь для временного хранения входной информации называется бу фером. В данном случае необходимый размер буфера нам заранее неиз вестен, и мы воспользуемся структурой данных под названием кольце вой буфер. Кольцевой буфер строится на основе вектора. Способ его ис пользования напоминает кольцо: этот вектор последовательно наполня етсявновьпоступающимисимволами,акогдаонзаполнитсяполностью, процесс начинается с начала, поверх уже существующих элементов. Ес ли нам заранее известно, что не понадобится хранить более n элемен тов, вектора длиной n будет достаточно, и перезапись элементов с нача ла не приведет к потере данных.
На рис. 7.1 показан код, реализующий операции с кольцевым буфером. Структура buf имеет пять полей: вектор для хранения объектов и четы ре индекса. Два из них, start и end, необходимы для любых операций с кольцевым буфером: start указывает на начальное значение в буфере и будет увеличиваться при изъятии элемента из буфера; end указывает на последнее значение в буфере и будет увеличиваться при добавлении нового элемента.
Два других индекса, used и new, потребуются для использования буфера в нашем приложении. Они могут принимать значение между start и end, причем всегда будет соблюдаться следующее соотношение:
start ≤ used ≤ new ≤ end
Пару used и new можно считать аналогом start и end для текущего сов падения. Когда мы начинаем отслеживать совпадение, used будет ра вен start, а new – end. Для каждого последовательного совпадения пары символов used будет увеличиваться. Когда used достигнет new, это будет означать, что мы считали из буфера все элементы, занесенные туда до начала проверки данного совпадения. Нам не нужно использовать боль ше символов, чем было в буфере перед началом нахождения текущего совпадения, иначе мы бы использовали некоторые символы по несколь ку раз. Поэтому нам требуется другой индекс, new, который исходно ра вен end, но не увеличивается при добавлении новых символов во время проверки совпадения.
Функция bref возвращает элемент, хранящийся в буфере по заданному индексу. Используя остаток от деления заданного индекса на длину вектора, мы можем сделать вид, что наш буфер имеет неограниченный размер. Вызов (new-buf n) создает новый буфер, способный хранить до n элементов.
Чтобы поместить в буфер новое значение, будем применять buf-insert. Эта функция увеличивает индекс end и кладет на это место заданный элемент. Обратной функцией является buf-pop, которая возвращает пер вый элемент буфера и увеличивает индекс start. Эти две функции нуж ны для любого кольцевого буфера.
138 Глава 7. Ввод и вывод
(defstruct buf
vec (start -1) (used -1) (new -1) (end -1))
(defun bref (buf n) (svref (buf-vec buf)
(mod n (length (buf-vec buf)))))
(defun (setf bref) (val buf n) (setf (svref (buf-vec buf)
(mod n (length (buf-vec buf))))
val))
(defun new-buf (len)
(make-buf :vec (make-array len)))
(defun buf-insert (x b)
(setf (bref b (incf (buf-end b))) x))
(defun buf-pop (b) (prog1
(bref b (incf (buf-start b))) (setf (buf-used b) (buf-start b)
(buf-new b) (buf-end b))))
(defun buf-next (b)
(when (< (buf-used b) (buf-new b)) (bref b (incf (buf-used b)))))
(defun buf-reset (b)
(setf (buf-used b) (buf-start b) (buf-new b) (buf-end b)))
(defun buf-clear (b)
(setf (buf-start b) -1 (buf-used b) -1 (buf-new b) -1 (buf-end b) -1))
(defun buf-flush (b str)
(do ((i (1+ (buf-used b)) (1+ i))) ((> i (buf-end b)))
(princ (bref b i) str)))
Рис. 7.1. Операции с кольцевым буфером
Следующие две функции написаны специально для нашего приложе ния: buf-next читает значение из буфера без его извлечения, buf-reset сбрасывает used и new до исходных значений start и end. Если все значе ния до new прочитаны, buf-next возвратит nil. Поскольку мы собираемся хранить в буфере исключительно символы, отличить nil как особое значение не будет проблемой.
Наконец, buf-flush выводит содержимое буфера, записывая все доступ ные элементы в заданный поток, а buf-clear очищает буфер, сбрасывая все индексы до -1.
7.4. Пример: замена строк |
139 |
Функции из кода на рис. 7.1 используются в коде на рис. 7.2, предостав ляющем средства для замены строк. Функция file-subst принимает че тыре аргумента: искомую строку, ее замену, входной и выходной фай лы. Она создает потоки, связанные с заданными файлами, и вызывает функцию stream-subst, которая и выполняет основную работу.
Вторая функция, stream-subst, использует алгоритм, который был схе матически описан в начале раздела. Каждый раз она читает из входно го потока один символ. Если он не совпадает с первым элементом иско мой строки, он тут же записывается в выходной поток (1). Когда начина ется совпадение, символы ставятся в очередь в буфер buf (2).
(defun file-subst (old new file1 file2) (with-open-file (in file1 :direction :input)
(with-open-file (out file2 :direction :output :if-exists :supersede)
(stream-subst old new in out))))
(defun stream-subst (old new in out) (let* ((pos 0)
(len (length old)) (buf (new-buf len)) (from-buf nil))
(do ((c (read-char in nil :eof)
(or (setf from-buf (buf-next buf))
(read-char in nil :eof)))) |
|
|
((eql c :eof)) |
|
|
(cond ((char= |
c (char old pos)) |
|
(incf pos) |
|
|
(cond ((= pos len) |
; 3 |
|
|
(princ new out) |
|
|
(setf pos 0) |
|
|
(buf-clear buf)) |
|
|
((not from-buf) |
; 2 |
|
(buf-insert c buf)))) |
|
((zerop |
pos) |
; 1 |
(princ |
c out) |
|
(when from-buf |
|
|
(buf-pop buf) |
|
|
(buf-reset buf))) |
|
|
(t |
|
; 4 |
(unless from-buf |
|
|
(buf-insert c buf)) |
|
|
(princ |
(buf-pop buf) out) |
|
(buf-reset buf) (setf pos 0))))
(buf-flush buf out)))
Рис. 7.2. Замена строк
140 |
Глава 7. Ввод и вывод |
Переменная pos указывает на положение символа в искомой строке. Ес ли оно равно длине самой строки, это означает, что совпадение найдено. В таком случае в выходной поток записывается замена строки, а буфер очищается (3). Если в какой-либо момент последовательность символов перестает соответствовать искомой строке, мы вправе забрать первый символ из буфера и записать в выходной поток, а также установить pos равным нулю, а сам буфер сбросить (4).
Следующая таблица наглядно демонстрирует, что происходит при за мене "baro" на "baric" в файле, содержащем только одно слово barbarous:
Символ |
Источник |
Совпадение |
Вариант |
Вывод |
Буфер |
b |
файл |
b |
2 |
|
b |
a |
файл |
a |
2 |
|
b a |
r |
файл |
r |
2 |
|
b a r |
b |
файл |
o |
4 |
b |
b.a r b. |
a |
буфер |
b |
1 |
a |
a.r b. |
r |
буфер |
b |
1 |
r |
r.b. |
b |
буфер |
b |
1 |
|
r b: |
a |
файл |
a |
2 |
|
r b:a |
r |
файл |
r |
2 |
|
r b:a r |
o |
файл |
o |
3 |
baric |
|
u |
файл |
b |
1 |
u |
|
s |
файл |
b |
1 |
s |
|
|
|
|
|
|
|
Первая колонка содержит текущий символ – значение переменной c; вторая показывает, откуда он был считан – из буфера или же прямиком из файла; третья показывает символ, с которым мы сравниваем, – эле мент old по индексу pos; четвертая указывает на вариант действия, со вершаемого в данном случае; пятая колонка соответствует текущему содержимому буфера после выполнения операции. В последней колон ке точками после символа показаны также позиции used и new. Если они совпадают, то это отображается с помощью двоеточия.
Предположим, что у нас есть файл "test1", содержащий следующий текст:
The struggle between Liberty and Authority is the most conspicuous feature in the portions of history with which we are earliest familiar, particularly in that of Greece, Rome, and England.
После выполнения (file-subst " th" " z" "test1" "test2") файл "test2"
будет иметь следующий вид:
The struggle between Liberty and Authority is ze most conspicuous feature in ze portions of history with which we are earliest familiar, particularly in zat of Greece, Rome, and England.