Skip to content
Find file
Fetching contributors…
Cannot retrieve contributors at this time
610 lines (504 sloc) 47.5 KB
\chapter{Макросы: стандартные управляющие конструкции}
\label{ch:07}
\thispagestyle{empty}
В~то время как многие из идей, появившихся в Lisp, от условных выражений до сборки
мусора, были добавлены в другие языки, есть одна особенность языка, которая продолжает
делать Common Lisp стоящим особняком от всех, это его система макросов. К сожалению, слово
\textit{макрос} описывает множество вещей в компьютерных науках, к которым макросы Common Lisp
имеют неявное и метафорическое отношение. Это приводит к бесконечным недопониманиям,
когда адепты Lisp пытаются объяснить другим, насколько макросы
замечательны\pclfootnote{Чтобы посмотреть на что это недопонимание похоже, найдите самую
длинную ветвь в Usenet из кросспостов между comp.lang.lisp и другой comp.lang.* группой
со словом \textit{macro} в заголовке. Примерные диалоги выглядят так:
Лиспер: <<Lisp является лучшим из-за своих макросов!>>;
Другой: <<Ты думаешь Lisp хорош \textit{из-за} макросов?! Но макросы ужасны и являются
злом; Lisp, должно быть, ужасен и является злом>>.}.
Чтобы понять макросы Lisp, необходимо подойти к делу со свободной головой, без
предубеждений, основанных на других вещах, которые также оказались названными словом
<<макросы>>. Итак, давайте начнём нашу тему с шага назад и обзора различных путей, которыми
создаются расширения в языках.
Всем программистам должна быть привычна идея о том, что определение языка может включать
стандартную библиотеку функций, которая строится на <<ядре>> языка,~-- библиотеку, которая
могла бы быть написана посредством языка любым прог\-рам\-мис\-том, если бы она не была
определена как часть стандартной библиотеки. Стандартная библиотека языка Си, например,
может быть написана почти полностью на переносимом Си. Аналогично большая часть всё
растущего набора классов и интерфейсов, которые поставляются в стандартном наборе Java
Development Kit (JDK), написана на <<чистом>> Java.
Одним из преимуществ определения языков в терминах <<ядро плюс стандартная библиотека>> является
лёгкость в понимании и воплощении. Однако реальная выгода заключается в выразительности~--
многое из того, про что вы думаете как про <<язык>>, на самом деле просто библиотека~-- язык
легко расширять. Если в Си нет функции для той или иной необходимой вам задачи, вы можете
её написать, и теперь у вас есть слегка улучшенная версия Си. Точно так же в языках, таких
как Java или Smalltalk, где почти все интересные части <<языка>> определены в терминах
классов, определяя новый класс, вы расширяете язык, делая его более подходящим для
написания программ, делающих то, что вам надо.
В~то время как Common Lisp поддерживает оба этих метода расширения языка, макросы дают
Lisp ещё один путь. Как было упомянуто кратко в главе~\ref{ch:04}, каждый макрос определяет свой
собственный синтаксис~-- определение того, как s-выражения, которые ему передаются, будут
превращены в Lisp-формы. С помощью макросов как части ядра языка возможно создавать
новые синтаксические управляющие конструкции, такие как \lstinline{WHEN}, \lstinline{DOLIST} и
\lstinline{LOOP}, а также формы определений вроде \lstinline{DEFUN} и \lstinline{DEFPARAMETER} как
часть <<стандартной библиотеки>> вместо встраивания их в ядро. Это имеет свои последствия
для реализации языка, но как программиста на Lisp вас будет больше заботить то, что это
даёт вам ещё один способ расширения языка, делая его языком, лучше подходящим для
выражения решений ваших собственных программистских проб\-лем.
В~данный момент может показаться, что преимущества от наличия ещё одного пути расширения
языка будет легко понять. Но по некоторой причине большое количество программистов,
которые фактически не использовали макросы Lisp и которые не задумываются о создании новых
функциональных абстракций или определений новых иерархий классов для решения своих задач,
панически боятся самой мысли о том, что они будут иметь возможность описания новых
синтаксических абстракций. Наиболее общей причиной <<макрофобии>>, похоже, является плохой
опыт от использования других <<макросистем>>. Простой страх перед неизвестным несомненно
также играет определённую роль. Чтобы избежать макрофобических реакций, я буду постепенно
знакомить вас с данным предметом, через обсуждение нескольких стандартных макросов и
конструкций контроля, определённых в Common Lisp. Это те вещи, которые нужно было бы
встроить в ядро языка, если бы в Lisp не было макросов. Когда вы используете их, вам не
надо беспокоиться, что они сделаны в виде макросов, но они представляют из себя хороший
пример того, что вы можете сделать с помощью макросов\footnote{Другим важным классом
языковых конструкций, которые сделаны на макросах, являются все конструкции-определения,
такие как \lstinline{DEFUN}, \lstinline{DEFPARAMETER}, \lstinline{DEFVAR} и др. В
главе~\ref{ch:24} вы будете определять ваши собственные макросы-определения, которые
позволят вам осмысленно писать код для чтения и записи двоичных данных.}\hspace{\footnotenegspace}. В~следующей
главе я покажу вам, как вы можете определять свои собственные макросы.
\section{WHEN и UNLESS}
Как вы уже видели, наиболее базовую форму условного выражения~-- \textit{если x, делай y;
иначе делай z}~-- представляет специальный оператор \lstinline{IF}, который имеет следующую
базовую форму:
\begin{myverb}
(if condition then-form [else-form])
\end{myverb}
\noindent{}\textit{condition} вычисляется, и если его значение не \lstinline{NIL}, тогда
\textit{then-form} выполняется и полученное значение возвращается. Иначе выполняется
\textit{else-form}, если она есть, и её значение возвращается. Если \textit{condition}
даёт \lstinline{NIL} и нет \textit{else-form}, тогда \lstinline{IF} возвращает \lstinline{NIL}.
\begin{myverb}
(if (> 2 3) "Yup" "Nope") ==> "Nope"
(if (> 2 3) "Yup") ==> NIL
(if (> 3 2) "Yup" "Nope") ==> "Yup"
\end{myverb}
Однако \lstinline{IF} не является вообще-то такой уж замечательной синтаксической
конструкцией, потому что \textit{then-form} и \textit{else-form} каждая ограничена одной
Lisp-формой. Это значит, что если вы хотите выполнить последовательность действий в
каком-либо из этих случаев, вам надо обернуть их в какой-то другой синтаксис. Например,
предположим, в середине программы спам-фильтра вы захотите сохранить в файле сообщение
как спам и обновить базу данных по спаму, если сообщение~-- спам. Вы не можете написать
так:
\begin{myverb}
(if (spam-p current-message)
(file-in-spam-folder current-message)
(update-spam-database current-message))
\end{myverb}
\noindent{}потому что вызов \lstinline{update-spam-database} будет принят за случай \textit{else}, а не
как часть ветви \textit{then}. Другой специальный оператор \lstinline{PROGN} выполняет любое
число форм по порядку и возвращает значение последней формы. Так что вы могли бы получить
желаемое, записав всё следующим образом:
\begin{myverb}
(if (spam-p current-message)
(progn
(file-in-spam-folder current-message)
(update-spam-database current-message)))
\end{myverb}
Это не так уж и ужасно. Однако, учитывая количество раз, когда вам придётся использовать
эту идиому, нетрудно представить себе, что через некоторое время это станет
утомительно. <<Почему>>~-- вы можете спросить у себя,~-- <<Lisp не предоставляет возможность
выразить то, что на самом деле мне надо, скажем ``Если x верно, делай то, то и ещё вот
это''?>> Другими словами, через некоторое время вы заметите повторяемость сочетания
\lstinline{IF} плюс \lstinline{PROGN} и захотите как-то абстрагироваться от этих деталей, вместо
того чтобы каждый раз заново переписывать их.
Это как раз то, что предоставляют макросы. В~данном случае Common Lisp поставляется со
стандартным макросом \lstinline{WHEN}, с которым всё можно написать так:
\begin{myverb}
(when (spam-p current-message)
(file-in-spam-folder current-message)
(update-spam-database current-message))
\end{myverb}
Но если бы он не был встроен в стандартную библиотеку, вы могли бы самостоятельно
определить \lstinline{WHEN} как макрос, используя запись с обратной кавычкой, которую я
обсуждал в главе~\ref{ch:03}\footnote{Вы не можете на самом деле использовать это определение в
Lisp, потому что незаконно переопределять имена из пакета \lstinline{COMMON-LISP}, откуда и
берётся \lstinline{WHEN}. Но если вы по-настоящему хотите попробовать написать этот макрос,
вам надо изменить его имя, поменять на какое-то другое, например на \lstinline{my-when}.}\hspace{\footnotenegspace}:
\begin{myverb}
(defmacro when (condition &rest body)
`(if ,condition (progn ,@body)))
\end{myverb}
Сопутствующим макросу \lstinline{WHEN} является \lstinline{UNLESS}, который оборачивает условие,
выполняя формы из тела, только если условие ложно. Другими словами:
\begin{myverb}
(defmacro unless (condition &rest body)
`(if (not ,condition) (progn ,@body)))
\end{myverb}
Конечно, это довольно тривиальные макросы. Тут нет никакой страшной чёрной магии; они
просто абстрагируют некоторые детали языковой бухгалтерии, позволяя вам выражать свои
намерения немного более ясно. Но их тривиальность имеет важное значение: так как система
макросов встроена в язык, вы можете писать тривиальные макросы вроде \lstinline{WHEN} и
\lstinline{UNLESS}, которые дают вам небольшую, но реальную выгоду в ясности, которая затем
умножается в тысячу раз, когда вы используете их. В~главах~\ref{ch:24}, \ref{ch:26}
и~\ref{ch:31} вы увидите, как макросы могут быть использованы для серьёзных вещей,
создавая целый предметно-ориентированный (domain-specific), встроенный язык. Но сначала
давайте закончим наше обсуждение стандартных макросов управления.
\section{COND}
Ещё один случай, когда непосредственное \lstinline{IF} выражение может оказаться ужасным,~-- это
когда у вас есть условное выражение с множественными ветвлениями: \textit{если A, делай X,
иначе, если B, делай Y; иначе делай Z}. Нет никакой логической проблемы в написании
такой цепочки условных выражений с \lstinline{IF}, но получится не очень красиво.
\begin{myverb}
(if a
(do-x)
(if b
(do-y)
(do-z)))
\end{myverb}
И это будет выглядеть ещё более ужасно, если вам понадобится включить множество форм для
\textit{then}-случаев, привлекая несколько \lstinline{PROGN}. Так что ничего удивительного, что
Common Lisp предоставляет макрос для выражения условия со множеством ветвлений:
\lstinline{COND}. Вот базовый вид:
\begin{myverb}
(cond
(test-1 form*)
.
.
.
(test-N form*))
\end{myverb}
Каждый элемент в теле представляет одну ветвь условия и состоит из списка, содержащего
форму условия и ноль или более форм для выполнения, если выбрана эта ветвь. Условия
вычисляются в том порядке, в каком расположены ветви до тех пор, пока одно из них не даст
истину. В~этой точке оставшиеся формы из ветви выполняются и значение последней формы
ветви возвращается как результат работы всего \lstinline{COND}. Если ветвь не содержит форм
после условия, то возвращается само значение условия. По соглашению ветвь, представляющая
последний случай \textit{else} в цепочке \textit{if/else-if}, записывается с условием
\lstinline{T}. Подойдёт любое не \lstinline{NIL}-значение, но \lstinline{T} служит дорожным знаком при
чтении кода. Таким образом вы можете записать предыдущее вложенное \lstinline{IF} выражение,
используя \lstinline{COND}, вот так:
\begin{myverb}
(cond (a (do-x))
(b (do-y))
(t (do-z)))
\end{myverb}
\section{AND, OR и NOT}
При написании условий в \lstinline{IF}, \lstinline{WHEN}, \lstinline{UNLESS} и \lstinline{COND} формах три
оператора оказываются очень полезны, это булевы логические операторы \lstinline{AND}, \lstinline{OR}
и \lstinline{NOT}.
\lstinline{NOT}~-- это функция, которая, строго говоря, не относится к этой главе, но она очень
тесно связана с \lstinline{AND} и \lstinline{OR}. Она берёт свой аргумент и обращает его значение
истинности, возвращая \lstinline{T}, если аргумент \lstinline{NIL}, и \lstinline{NIL} в ином случае.
\lstinline{AND} и \lstinline{OR}, однако, являются макросами. Они представляют логические конъюнкцию
и дизъюнкцию произвольного числа подформ и определены как макросы, так что они оптимальны
в выполнении. Это значит, что они вычисляют ровно столько своих подформ, в порядке слева
направо, сколько необходимо для конечного значения. То есть \lstinline{AND} останавливается и
возвращает \lstinline{NIL} сразу же, как только одна из подформ выдаст \lstinline{NIL}. Если все
подформы выдают не \lstinline{NIL}-результат, она возвращает значение последней
подформы. \lstinline{OR}, с другой стороны, останавливается, как только одна из подформ выдаст
не \lstinline{NIL} и возвращает полученное значение. Если ни одна из подформ не выдаст истину,
\lstinline{OR} возвращает \lstinline{NIL}. Вот несколько примеров:
\begin{myverb}
(not nil) ==> T
(not (= 1 1)) ==> NIL
(and (= 1 2) (= 3 3)) ==> NIL
(or (= 1 2) (= 3 3)) ==> T
\end{myverb}
\section{Циклы}
Управляющие конструкции~-- ещё один вид циклических конструкций в LISP\translationnote
дополнение к рекурсии.}. Циклические средства в Common Lisp, в дополнение к
мощности и гибкости, являются интересным уроком по программированию в стиле <<получить всё
и сразу>>, чему поспособствовали макросы.
Как оказалось, ни один из 25 специальных операторов Lisp не поддерживает напрямую
структуру циклов. Все циклические конструкции контроля в Lisp~-- это макросы, построенные
на двух специальных операторах, которые представляют собой примитивное
\textit{goto}-средство\footnote{Специальными операторами, если вы хотите знать, являются
\lstinline{TAGBODY} и \lstinline{GO}. Сейчас нет необходимости обсуждать их, но я рассмотрю их в
главе~\ref{ch:20}.}\hspace{\footnotenegspace}. Как многие хорошие абстракции, синтаксические или нет, циклические
макросы в Lisp построены как набор слоёв абстракций, начиная с основы, которой являются те
два специальных оператора.
В~самом низу (оставляя в стороне специальные операторы) находится наиболее общая
конструкция контроля \lstinline{DO}. Хотя и очень мощный, \lstinline{DO} страдает, как и многие
абстракции общего назначения, от чрезмерности для простых ситуаций. Так что Lisp
предоставляет два других макроса, \lstinline{DOLIST} and \lstinline{DOTIMES}, которые менее гибки,
чем \lstinline{DO}, но лучше поддерживают наиболее распространённые случаи цикла по элементам
списка или цикла с подсчётом. Хотя конкретная реализация Lisp может реализовать эти
макросы как ей угодно, обычно они реализованы как макросы, которые раскрываются в
соответствующий \lstinline{DO} цикл. Таким образом, \lstinline{DO} предоставляет базовую структурную
конструкцию цикла поверх нижележащих примитивов, представленных специальными операторами
Common Lisp, а \lstinline{DOLIST} и \lstinline{DOTIMES} представляют две лёгкие в использовании,
хотя и менее общие конструкции. И, как вы увидите в следующей главе, вы можете строить
свои собственные конструкции цикла поверх \lstinline{DO} в ситуациях, где \lstinline{DOLIST} и
\lstinline{DOTIMES} вам не подходят.
Наконец, макрос \lstinline{LOOP} представляет собой полномасштабный мини-язык для выражения
циклических конструкций на не лисп-, а англоподобном (или, как минимум, алголоподобном)
языке. Некоторые хакеры Lisp любят \lstinline{LOOP}; другие ненавидят его. Фанаты \lstinline{LOOP}
любят его за то, что он предоставляет краткий способ выразить определённые, обычно
необходимые циклические конструкции. Его недоброжелатели не любят его, потому что он
недостаточно похож на остальной Lisp. Однако к какому бы лагерю вы не примкнули, это
замечательный пример возможностей макросов добавлять новые конструкции в язык.
\section{DOLIST и DOTIMES}
Я начну с лёгких в использовании макросов \lstinline{DOLIST} и \lstinline{DOTIMES}.
\lstinline{DOLIST} проходит по всем элементам списка, выполняя тело цикла с переменной,
содержащей последовательно элементы списка\footnote{\lstinline{DOLIST} похож на
\lstinline{foreach} из Perl или \lstinline{for} из Python. Java добавила похожую
конструкцию цикла вместе с <<улучшенным>> циклом \lstinline{for} в Java 1.5 как часть
JSR-201. Заметьте, какое отличие есть у макросов. Программист на Lisp, который заметил
общий шаблон в своём коде, может написать макрос, чтобы получить для себя абстракцию
этого шаблона на уровне исходников. Программист на Java, который заметил такой же
шаблон, должен убедить Sun, что этот частный шаблон стоит добавления в язык. Затем Sun
должна опубликовать JSR и созвать представителей промышленности, <<экспертную группу>>,
чтобы всё утвердить. Этот процесс, по словам Sun, занимает примерно 18 месяцев. После
этого все авторы компиляторов должны обновить свои компиляторы для поддержки нового
свойства. И даже когда любимый компилятор программиста на Java поддержит новую версию
Java, он, возможно, не сможет использовать новую особенность до тех пор, пока ему не
позволят нарушить совместимость исходных кодов с предыдущей версией Java. Так
неудобство, которое программист на Common Lisp может разрешить для себя за пять минут,
может портить жизнь программисту на Java годами.}\hspace{\footnotenegspace}. Вот базовый скелет (оставляя
некоторые эзотерические опции):
\begin{myverb}
(dolist (var list-form)
body-form*)
\end{myverb}
Когда цикл стартует, \textit{list-form} выполняется один раз, чтобы создать список. Затем
тело цикла выполняется для каждого элемента в списке, с переменной \textit{var},
содержащей значение элемента. Например:
\begin{myverb}
CL-USER> (dolist (x '(1 2 3)) (print x))
1
2
3
NIL
\end{myverb}
Использованная таким образом, форма \lstinline{DOLIST} в целом возвращает \lstinline{NIL}.
Если вы хотите прервать цикл \lstinline{DOLIST} до окончания списка, можете использовать
\lstinline{RETURN}.
\begin{myverb}
CL-USER> (dolist (x '(1 2 3)) (print x) (if (evenp x) (return)))
1
2
NIL
\end{myverb}
\lstinline{DOTIMES}~-- это конструкция цикла верхнего уровня для циклов с подсчётом. Основной
вид более-менее такой же, как у \lstinline{DOLIST}.
\begin{myverb}
(dotimes (var count-form)
body-form*)
\end{myverb}
\noindent{}\textit{count-form} должна выдать целое число. Каждый раз в процессе цикла \textit{var}
содержит последовательные целые от 0 до на единицу меньшего, чем то число. Например:
\begin{myverb}
CL-USER> (dotimes (i 4) (print i))
0
1
2
3
NIL
\end{myverb}
Так же, как и с \lstinline{DOLIST}, вы можете использовать \lstinline{RETURN}, чтобы прервать цикл
раньше.
Поскольку тела обоих \lstinline{DOLIST} и \lstinline{DOTIMES} циклов могут содержать любые типы
выражений, вы также можете делать циклы вложенными. Например, чтобы напечатать таблицу
умножения от $1 \times 1 = 1$ до $20 \times 20 = 400$, вы можете написать такую пару вложенных циклов
\lstinline{DOTIMES}:
\begin{myverb}
(dotimes (x 20)
(dotimes (y 20)
(format t "~3d " (* (1+ x) (1+ y))))
(format t "~%"))
\end{myverb}
\section{DO}
Хотя \lstinline{DOLIST} и \lstinline{DOTIMES} удобны и легки в использовании, они недостаточно
гибки, чтобы использоваться для любых циклов. Например, что, если вы захотите менять на
каждом шаге несколько переменных параллельно? Или использовать произвольное выражение для
проверки окончания цикла? Если ни \lstinline{DOLIST}, ни \lstinline{DOTIMES} не подходят для ваших
целей, у вас всё ещё есть доступ к наиболее общему циклу \lstinline{DO}.
Там, где \lstinline{DOLIST} и \lstinline{DOTIMES} предоставляют только одну переменную цикла,
\lstinline{DO} позволяет вам держать любое число переменных и даёт вам полный контроль над тем,
как они будут изменяться на каждом шаге цикла. Вы также определяете проверку, которая
говорит, когда циклу завершиться, и может предоставлять форму для вычисления в конце цикла,
чтобы сформировать возвращаемое значение для всего \lstinline{DO} в целом. Базовый шаблон
выглядит так:
\begin{myverb}
(do (variable-definition*)
(end-test-form result-form*)
statement*)
\end{myverb}
Каждое \textit{variable-definition} (определение переменной) вводит переменную, которая
будет в поле видимости тела цикла. Полная форма одного определения переменной,~-- это список,
содержащий три элемента.
\begin{myverb}
(var init-form step-form)
\end{myverb}
\noindent{}\textit{init-form} будет выполнена в начале цикла, и полученное значение присвоено
переменной \textit{var}. Перед каждой последующей итерацией цикла \textit{step-form}
будет выполнена, и её значение присвоено \textit{var}. Форма \textit{step-form}
необязательна; если её не будет, переменная останется с тем же значением от итерации к
итерации, пока вы прямо не назначите ей новое значение в теле цикла. Так же, как и с
присвоением переменным значений в \lstinline{LET}, если форма \textit{init-form} не задана,
переменной присваивается \lstinline{NIL}. Так же, как и в \lstinline{LET}, вы можете использовать
просто имя переменной вместо списка, содержащего только имя.
В~начале каждой итерации, после того как все переменные цикла получили свои новые
значения, выполняется форма \textit{end-test-form}. До тех пор, пока она вычисляется в
\lstinline{NIL}, итерация происходит, выполняя \textit{statement} по порядку.
Когда вычисление формы \textit{end-test-form} выдаст истину, будет вычислена форма
\textit{result-form}, и значение, полученное в результате, будет возвращено как значение
всего выражения \lstinline{DO}.
На каждом шаге итерации шаговые формы для переменных вычисляются прежде придания новых
значений этим переменным. Это значит, что вы можете ссылаться на любую другую переменную
внутри шаговых форм\footnote{Вариант \lstinline{DO}, \lstinline{DO*}, назначает каждой переменной
её значение перед вычислением шаговой формы для последующих переменных. За деталями
обратитесь к руководству по выбранному вами Common Lisp.}\hspace{\footnotenegspace}. Таким образом, цикл выглядит
так:
\begin{myverb}
(do ((n 0 (1+ n))
(cur 0 next)
(next 1 (+ cur next)))
((= 10 n) cur))
\end{myverb}
Шаговые формы \textit{(1+ n)}, \textit{next}, и \textit{(+ cur next)} все вычисляются,
используя старое значение \textit{n}, \textit{cur} и \textit{next}. Только после
вычисления всех шаговых форм переменные получают свои новые значения. (Математически
образованные читатели могут заметить, что это частично эффективный способ подсчёта
одиннадцатого числа Фибоначчи.)
Этот пример также иллюстрирует ещё одну характеристику \lstinline{DO}~-- так как вы можете
изменять на каждом шаге несколько переменных, вам зачастую не понадобится тело цикла
вообще. В~другой раз вы можете обойтись без результирующей формы, в частности если вы
используете цикл как конструкцию контроля. Эта гибкость, однако, является причиной, по
которой выражение \lstinline{DO} может стать плохо читаемым. Что, собственно, все эти скобки
здесь делают? Лучший способ понять \lstinline{DO} выражение,~-- это держать в голове основной
шаблон.
\begin{myverb}
(do (variable-definition*)
(end-test-form result-form*)
statement*)
\end{myverb}
Шесть скобок в этом шаблоне,~-- это только те, которые требуются самим \lstinline{DO}. Вам нужна
одна пара для закрытия описания переменных, одна пара для закрытия теста окончания и
результирующей формы и одна пара для закрытия всего выражения. Другие формы внутри
\lstinline{DO} могут потребовать своих собственных пар скобок~-- определения переменных, это
обычно списки, например. К тому же форма проверки окончания,~-- это зачастую функция. Однако
скелет \lstinline{DO} цикла всегда будет одним и тем же. Вот несколько примеров цикла \lstinline{DO}
со скелетом, отмеченным фигурными скобочками вместо обычных\translationnote{Они используются только для
демонстрационных целей~-- это не часть стандартного синтаксиса CL.}:
\begin{myverb}
{do {(i 0 (1+ i))}
{(>= i 4)}
(print i)}
\end{myverb}
Заметьте, что форма для результата пропущена. Это, однако, не особо распространённое
использование \lstinline{DO}, так как такой цикл гораздо проще написать, используя
\lstinline{DOTIMES}\footnote{\lstinline{DOTIMES} также предпочтительнее, потому что раскрытие
макроса будет, скорее всего, включать декларации, которые позволяют компилятору
генерировать более эффективный код.}\hspace{\footnotenegspace}.
\begin{myverb}
(dotimes (i 4) (print i))
\end{myverb}
Другой пример, в котором отсутствует тело цикла для вычисления чисел Фибоначчи:
\begin{myverb}
(do ((n 0 (1+ n))
(cur 0 next)
(next 1 (+ cur next)))
((= 10 n) cur))
\end{myverb}
Наконец, следующий пример демонстрирует цикл \lstinline{DO}, в котором нет привязанных
переменных. Он крутится, пока текущее время меньше, чем значение глобальной переменной,
печатая \lstinline{"Waiting"} каждую минуту. Заметьте, что даже без переменных цикла вы всё равно
нуждаетесь в пустом списке для списка переменных.
\begin{myverb}
(do ()
((> (get-universal-time) *some-future-date*))
(format t "Waiting~%")
(sleep 60))
\end{myverb}
\section{Всемогущий LOOP}
Для простых случаев у вас есть \lstinline{DOLIST} и \lstinline{DOTIMES}. И если они не удовлетворяют
вашим нуждам, вы можете вернуться к совершенно общему \lstinline{DO}. Чего ещё можно хотеть?
Однако оказывается, что удобные идиомы для циклов появляются снова и снова, такие как
циклы по элементам различных структур с данными: списков, векторов, хэш-таблиц и пакетов,
либо накопление значений разными способами в процессе цикла: собирание, подсчёт,
суммирование, минимизация или максимизация. Если вы нуж\-дае\-тесь в цикле, который бы делал
одну из этих вещей (или несколько одновременно), макрос \lstinline{LOOP} может предоставить вам
простой путь это выразить.
Макрос \lstinline{LOOP} на самом деле бывает двух видов: простой и расширенный. Простая версия
проста как только можно: бесконечный цикл без связанных с ним переменных. Скелет выглядит
так:
\begin{myverb}
(loop
body-form*)
\end{myverb}
Формы внутри тела выполняются каждый раз в процессе цикла, который будет длиться вечно,
пока вы не используете \lstinline{RETURN}, чтобы прервать его. Например, вы могли бы записать
предыдущий \lstinline{DO} цикл с простым \lstinline{LOOP}.
\begin{myverb}
(loop
(when (> (get-universal-time) *some-future-date*)
(return))
(format t "Waiting~%")
(sleep 60))
\end{myverb}
Расширенный \lstinline{LOOP}~-- несколько иной зверь. Он отличается использованием спе\-циаль\-ных
ключевых слов, которые представляют язык специального назначения для выражения различных
конструкций циклов. Это ничего не значит, что не все лисповоды любят язык расширенного
\lstinline{LOOP}. Как минимум один из создателей Common Lisp ненавидит его. Критики
\lstinline{LOOP} жалуются, что его синтаксис совсем не лисповский (другими словами, в нём
недостаточно скобок). Любители \lstinline{LOOP} замечают, что в этом-то и весь смысл: сложные
циклические конструкции достаточно тяжелы для восприятия и без заворачивания их в туманный
синтаксис \lstinline{DO}. Лучше, говорят они, иметь немного более наглядный синтаксис, который
давал бы вам какие-то подсказки о том, что происходит.
Вот, например, характерный цикл \lstinline{DO}, который собирает числа от 1 до 10 в список:
\begin{myverb}
(do ((nums nil) (i 1 (1+ i)))
((> i 10) (nreverse nums))
(push i nums)) ==> (1 2 3 4 5 6 7 8 9 10)
\end{myverb}
У опытного лиспера не будет никаких проблем понять этот код~-- это просто вопрос
понимания основной формы \lstinline{DO} цикла и распознания \lstinline{PUSH}/\lstinline{NREVERSE} идиомы
для построения списка. Но это не совсем прозрачно. Версия с \lstinline{LOOP}, с другой стороны,
почти понятна как предложение на английском (цикл по \textit{i} от 1 до 10, собирая
\textit{i}):
\begin{myverb}
(loop for i from 1 to 10 collecting i) ==> (1 2 3 4 5 6 7 8 9 10)
\end{myverb}
Далее ещё несколько примеров простого использования \lstinline{LOOP}. Вот сумма квадратов
первых десяти чисел:
\begin{myverb}
(loop for x from 1 to 10 summing (expt x 2)) ==> 385
\end{myverb}
Это~-- подсчёт числа гласных в строке:
\begin{myverb}
(loop for x across "the quick brown fox jumps over the lazy dog"
counting (find x "aeiou")) ==> 11
\end{myverb}
Вот вычисление одиннадцатого числа Фибоначчи, аналогично использованному ранее циклу \lstinline{DO}:
\begin{myverb}
(loop for i below 10
and a = 0 then b
and b = 1 then (+ b a)
finally (return a))
\end{myverb}
Символы \lstinline{across}, \lstinline{and}, \lstinline{below}, \lstinline{collecting}, \lstinline{counting},
\lstinline{finally}, \lstinline{for}, \lstinline{from}, \lstinline{summing}, \lstinline{then} и \lstinline{to} являются
некоторыми из ключевых слов цикла, чьё присутствие обозначает, что перед нами расширенная
версия \lstinline{LOOP}\footnote{Ключевые слова цикла,~-- это несколько неудачный термин, так
как они не являются ключевыми словами-символами. Фактически \lstinline{LOOP} не волнует
пакет, из которого эти символы. Когда макрос \lstinline{LOOP} разбирает своё тело, он
считает, что любые, с соответствующим названием символы эквивалентны. Вы можете даже
использовать настоящие ключевые слова, если хотите~-- \lstinline{:for}, \lstinline{:across} и так
далее, потому что они также имеют правильное название. Но большинство людей просто
использует простые символы. Потому что ключевые слова в цикле применяются только как
синтаксические маркеры, и даже не важно, если они уже использованы для других целей~--
как имена для функций или переменных.}\hspace{\footnotenegspace}.
Я приберегу подробности о \lstinline{LOOP} для главы~\ref{ch:22}, однако сейчас стоит заметить,
что это ещё один пример того, как макросы могут быть использованы для расширения основы
языка. В~то время как \lstinline{LOOP} предоставляет свой собственный язык для выражения
циклических конструкций, он никак не отрезает вас от остального Lisp. Ключевые слова
\lstinline{LOOP} разбираются в соответствии с его грамматикой, но остальной код внутри
\lstinline{LOOP},~-- это обычный Lisp-код.
И также стоит отметить ещё раз, что хотя макрос \lstinline{LOOP} гораздо более сложный, чем
\lstinline{WHEN} или \lstinline{UNLESS}, он просто ещё один макрос. Если бы он не был включён в
стандартную библиотеку, вы могли бы сделать это сами или взять стороннюю библиотеку,
которая это сделает.
На этом я завершу наш тур по основным управляющим конструкциям, реализованным при помощи
макросов. Теперь вы готовы взглянуть поближе на то, как определять свои собственные
макросы.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End:
Something went wrong with that request. Please try again.