Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
848 lines (706 sloc) 71.8 KB
\chapter{Макросы: создание собственных макросов}
\label{ch:08}
\thispagestyle{empty}
Теперь пора начать писать свои собственные макросы. Стандартные макросы, описанные мною в
предыдущей главе, должны были дать вам некоторое представление о том, что вы можете
сделать при помощи макросов, но это было только начало. Поддержка макросов в Common Lisp
не является чем-то большим, чем поддержка функций в C, и поэтому каждый программист на
Lisp может создать свои собственные варианты стандартных конструкций контроля точно так
же, как каждый программист на C может написать простые варианты функций из стандартной
библиотеки C. Макросы являются частью языка, которая позволяет вам создавать абстракции
поверх основного языка и стандартной библиотеки, что приближает вас к возможности
непосредственного выражения того, что вы хотите выразить.
Возможно, самым большим препятствием для правильного понимания макросов является, как это
ни парадоксально, то, что они так хорошо интегрированы в язык. Во многих отношениях они
кажутся просто странной разновидностью функций~-- они написаны на Lisp, они принимают
аргументы и возвращают результаты, и они позволяют вам абстрагироваться от отвлекающих
деталей. Тем не менее, несмотря на эти многочисленные сходства, макросы работают на
другом, по сравнению с функциями, уровне и создают совершенно иной вид абстракции.
Как только вы поймёте разницу между макросами и функциями, тесная интеграция макросов в
язык станет огромным благом. И в то же время для новых лисперов это часто является
источником путаницы. Следующая история, не являющаяся подлинной в историческом или
техническом смысле, попытается уменьшить ваше замешательство, направляя ваши мысли
касательно работы макросов в правильное русло.
\section{История Мака: обычная такая история}
Когда-то давным-давно жила-была компания Lisp-программистов. Это было так давно, что в
Lisp даже не существовало макросов. Каждый раз все то, что не могло быть определено с
помощью функций или сделано с помощью специализированных операторов, должно было быть
написано в полном объёме, что было довольно трудоёмким делом. К сожалению, программисты в
этой компании были хоть и блестящи, но очень ленивы. Нередко в своих программах, когда
процесс написания больших объёмов кода становился слишком утомителен, они вместо кода
писали комментарии, описывающие требуемый в этом месте программы код. К ещё большему
сожалению, из-за своей лени программисты также ненавидели возвращаться назад и
действительно писать код, описанный в комментариях. Вскоре компания получила большую кучу
кода, которую никто не мог запустить, потому что он был полон комментариев с описанием
того, что ещё предстоит написать.
В~отчаянии большой босс нанял младшего (junior) программиста, Мака, чьей работой стали
поиск комментариев, написание требуемого кода и вставка его в программу на место
комментариев. Мак никогда не запускал программы, ведь они не были завершены, и поэтому он
попросту не мог этого сделать. Но даже если бы они были завершены, Мак не знал, какие
данные необходимо подать на их вход. Поэтому он просто писал свой код, основываясь на
содержимом комментариев, и посылал его назад создавшему комментарий программисту.
С помощью Мака все программы вскоре были доделаны, и компания заработала уйму денег,
продавая их: так много денег, что смогла удвоить количество программистов. Но по какой-то
причине никто не думал нанимать кого-то в помощь Маку; вскоре он один помогал нескольким
дюжинам программистов. Чтобы не тратить все своё время на поиск комментариев в исходном
коде, Мак внёс небольшие изменения в используемый программистами компилятор. Теперь если
компилятор встречал комментарий, то отсылал его электронной почтой Маку, а затем ждал
ответа с замещающим комментарий кодом. К сожалению, даже с этими изменениями Маку было
тяжело удовлетворять запросам программистов. Он работал так тщательно, как только мог, но
иногда, особенно когда записи не были ясны, он допускал ошибки.
Однако программисты обнаружили, что чем точнее они пишут свои комментарии, тем больше
вероятность того, что Мак вернёт правильный код. Как-то раз один из программистов,
встретив затруднение с описанием в словах нужного кода, включил в один из комментариев
программу на Lisp, которая генерировала нужный код. Такой комментарий был удобен Маку: он
просто запустил программу и послал результат компилятору.
Следующее новшество появилось, когда программист вставил в самый верх одной из своих
программ комментарий, содержащий определение функции и пояснение, гласившее: <<Мак, не пиши
здесь никакого кода, но сохрани эту функцию на будущее; я собираюсь использовать её в
некоторых своих комментариях>>. Другие комментарии в этой программе гласили следующее:
<<Мак, замени этот комментарий на результат выполнения той функции с символами \lstinline{x} и
\lstinline{y} в качестве аргументов>>.
Этот метод распространился так быстро, что в течение нескольких дней большинство программ
стало содержать дюжины комментариев с описанием функций, которые использовались только
кодом в других комментариях. Чтобы облегчить Маку различение комментариев, содержащих
только определения и не требующих немедленного ответа, программисты отмечали их
стандартным предисловием: <<Definition for Mac, Read Only>> (Определение для Мака, только
для чтения). Это (как мы помним, программисты были очень ленивы) быстро сократилось до
<<DEF. MAC. R/O>>, а потом до <<DEFMACRO>>.
Очень скоро в комментариях для Мака вообще не осталось английского. Целыми днями он читал
и отвечал на электронные письма от компилятора, содержащие
\lstinline{DEFMACRO}-комментарии, и вызывал функции, описанные в \lstinline{DEFMACRO}. Так
как Lisp-программы в комментариях осуществляли всю реальную работу, то работа с
элект\-рон\-ны\-ми письмами перестала быть проблемой. У Мака внезапно стало много свободного
времени, и он сидел в своём кабинете и грезил о белых песчаных пляжах, чистой голубой
океанской воде и напитках с маленькими бумажными зонтиками.
Несколько месяцев спустя программисты осознали, что Мака уже довольно давно никто не
видел. Придя в его кабинет, они обнаружили, что все покрыто тонким слоем пыли, стол усыпан
брошюрами о различных тропических местах, а компьютер выключен. Но компилятор продолжал
работать! Как ему это удавалось? Выяснилось, что Мак сделал заключительное изменение в
компиляторе: вместо отправки электронного письма с комментарием Маку компилятор теперь
сохранял функции, описанные с помощью \lstinline{DEFMACRO}-комментариев, и запускал при
вызове их из других комментариев. Программисты решили, что нет оснований говорить большим
боссам, что Мак больше не приходит на работу. Так происходит и по сей день: Мак получает
зарплату и время от времени шлёт программистам открытки то из одной тропической страны, то
из другой.
\section{Время раскрытия макросов против времени \mbox{выполнения}}
Ключом к пониманию макросов является полное понимание разницы между кодом, генерирующим
код (макросами), и кодом, который в конечном счёте выполняет программу (все
остальное). Когда вы пишете макросы, то пишете программы, которые будут использоваться
компилятором для генерации кода, который затем будет скомпилирован. Только после того, как
все макросы будут полностью раскрыты, а полученный код скомпилирован, программа сможет
быть запущена. Время, когда выполняются макросы, называется \textit{временем раскрытия
макросов}; оно отлично от \textit{времени выполнения}, когда выполняется обычный код,
включая код, сгенерированный макросами.
Очень важно полностью понимать это различие, так как код, работающий во время раскрытия
макросов, запускается в окружении, сильно отличающемся от окружения кода, работающего во
время выполнения. А именно во время раскрытия макросов не существует способа получить
доступ к данным, которые будут существовать во время выполнения. Подобно Маку, который не
мог запускать программы, над которыми он работал, так как не знал, что является корректным
входом для них, код, работающий во время раскрытия макросов, может работать только с
данными, являющимися неотъемлемой частью исходного кода. Для примера предположим, что
следующий исходный код появляется где-то в программе:
\begin{myverb}
(defun foo (x)
(when (> x 10) (print 'big)))
\end{myverb}
Обычно вы бы думали о \lstinline{x} как о переменной, которая будет содержать аргумент,
переданный при вызове foo. Но во время раскрытия макросов (например, когда компилятор
выполняет макрос \lstinline{WHEN}) единственными доступными данными является исходный
код. Так как программа пока не выполняется, нет вызова \lstinline{foo} и, следовательно, нет
значения, ассоциированного с \lstinline{x}. Вместо этого значения, которые компилятор передаёт
в \lstinline{WHEN}, являются списками Lisp, представляющими исходный код, а именно
\lstinline{(> x 10)} и \lstinline{(print 'big)}. Предположим, что \lstinline{WHEN} определён, как вы видели в
предыдущей главе, подобным образом:
\begin{myverb}
(defmacro when (condition &rest body)
`(if ,condition (progn ,@body)))
\end{myverb}
При компиляции кода \lstinline{foo} макрос \lstinline{WHEN} будет запущен с этими двумя формами в
качестве аргументов. Параметр \lstinline{condition} будет связан с формой \lstinline{(> x 10)}, а
форма \lstinline{(print 'big)} будет собрана (will be collected) в список (и будет его
единственным элементом), который станет значением параметра \lstinline!&rest!
\lstinline{body}. Выражение квазицитирования затем сгенерирует следующий код:
\begin{myverb}
(if (> x 10) (progn (print 'big)))
\end{myverb}
\noindent{}подставляя значение \lstinline{condition}, а также вклеивая значение body в \lstinline{PROGN}.
Когда Lisp интерпретируется, а не компилируется, разница между временем раскрытия макросов
и временем выполнения менее очевидна, так как они <<переплетены>> во времени (temporally
intertwined). Также стандарт языка не специфицирует в точности того, как интерпретатор должен
обрабатывать макросы: он может раскрывать все макросы в интерпретируемой форме, а затем
интерпретировать полученный код, или же он может начать непосредственно с интерпретирования
формы и раскрывать макросы при их встрече. В~обоих случаях макросам всегда передаются
невычисленные объекты Lisp, представляющие подформы формы макроса, и задачей макроса все
также является генерирование кода, который затем осуществит какие-то действия, а не
непосредственное осуществление этих действий.
\section{DEFMACRO}
Как вы видели в главе~\ref{ch:03}, макросы на самом деле определяются с помощью форм
\lstinline{DEFMACRO}, что означает, разумеется, <<DEFine MACRO>>, а не <<Definition for
Mac>>. Базовый шаблон \lstinline{DEFMACRO} очень похож на шаблон \lstinline{DEFUN}.
\begin{myverb}
(defmacro name (parameter*)
"Optional documentation string."
body-form*)
\end{myverb}
Подобно функциям, макрос состоит из имени, списка параметров, необязательной строки
документации и тела, состоящего из выражений Lisp\pclfootnote{Подобно функциям, макрос также
может содержать объявления, но сейчас вам не стоит беспокоиться об этом.}. Однако, как я
только что говорил, работой макроса не является осуществление какого-то действия
напрямую~-- его работой является генерирование кода, который затем сделает то, что вам
нужно.
Макросы могут использовать всю мощь Lisp при генерировании своих раскрытий, поэтому в этой
главе я смогу дать лишь обзор того, что вы можете делать с по\-мощью макросов. Однако я могу
описать общий процесс написания макросов, который подходит для всех типов макросов, от
самых простых до наиболее сложных.
Задачей макроса является преобразование формы макроса (другими словами, формы Lisp, первым
элементом которой является имя макроса) в код, который осуществляет определённые
действия. Иногда вы пишете макрос, начиная с того кода, который вы бы хотели иметь
возможность писать, то есть с примера формы макроса. В~другой раз вы решаете написать
макрос после того, как вы использовали какой-то образец кода несколько раз, и понимаете,
что можете сделать ваш код чище путём абстрагирования этого образца.
Несмотря на то, с какого конца вы начинаете, вы должны представлять противоположный конец,
перед тем как сможете начать создавать макрос: вы должны знать и то, откуда движетесь, и
то, куда, до того как сможете рассчитывать написать код, осуществляющий это
автоматически. Таким образом, первым шагом в написании макроса является написание по
крайней мере одного примера вызова макроса и кода, в который этот вызов должен
раскрыться.
После того как у вас есть пример вызова и его желаемое раскрытие, вы готовы ко второму
шагу: фактическому написанию кода макроса. Для простых макросов это будет тривиальным
делом написания шаблона-квазицитирования с параметрами макроса, вставленными на нужные
места. Сложные макросы сами будут значительными программами, использующими вспомогательные
функции и структуры данных.
После того как вы написали код, преобразующий пример вызова в соответствующее раскрытие,
вам нужно убедиться в том, что у абстракции, предоставляемой макросом, нет <<протечек>>
деталей реализации. Предоставляемые макросами <<дырявые>> абстракции будут работать хорошо
только для определённых аргументов или будут взаимодействовать с кодом вызывающего
окружения нежелательными способами. Как оказывается, макросы могут <<протекать>> лишь
небольшим количеством способов, всеъ из которых легко избежать, если вы знаете, как
выявлять их. Я обсужу, как это делается, в разделе <<Устранение протечек>>.
Подводя итог, можно сказать, что шаги по написанию макросов следующие:
\begin{enumerate}
\item Написание примера вызова макроса, а затем кода, в который он должен быть раскрыт
(или в обратном порядке).
\item Написание кода, генерирующего написанный вручную код раскрытия по аргументам в
примере вызова.
\item Проверка того, что предоставляемая макросом абстракция не <<протекает>>.
\end{enumerate}
\section{Пример макроса: do-primes}
Для того чтобы увидеть, как этот трёхшаговый процесс осуществляется, вы напишете макрос
\lstinline{do-primes}, который предоставляет конструкцию итерирования, подобную
\lstinline{DOTIMES} и \lstinline{DOLIST}, за исключением того, что вместо итерирования по целым
числам или элементам списка итерирование будет производиться по последовательным простым
числам. Этот пример не является примером чрезвычайно полезного макроса, он~-- всего лишь
средство демонстрации вышеописанного процесса.
Прежде всего вам нужны две вспомогательные функции: одна для проверки того, является ли
данное число простым, и вторая, возвращающая следующее простое число, большее или равное
её аргументу. В~обоих случаях вы можете использовать простой, но неэффективный метод
<<грубой силы>>.
\begin{myverb}
(defun primep (number)
(when (> number 1)
(loop for fac from 2 to (isqrt number)
never (zerop (mod number fac)))))
(defun next-prime (number)
(loop for n from number when (primep n) return n))
\end{myverb}
Теперь вы можете написать макрос. Следуя процедуре, очерченной выше, вам нужен, по крайней
мере, один пример вызова макроса и кода, в который он должен быть раскрыт. Предположим, что
вы начали с мысли о том, что хотите иметь возможность написать следующее:
\begin{myverb}
(do-primes (p 0 19)
(format t "~d " p))
\end{myverb}
для выражения цикла, который выполняет тело для каждого простого числа, большего либо
равного 0 и меньшего либо равного 19, используя переменную \lstinline{p} для хранения
очередного простого числа. Имеет смысл смоделировать этот макрос с помощью стандартных
макросов \lstinline{DOLIST} и \lstinline{DOTIMES}; макрос, следующий образцу существующих
макросов, легче понять и использовать, нежели макросы, которые вводят неоправданно новый
синтаксис.
Без использования макроса \lstinline{do-primes} вы можете написать такой цикл путём
использования \lstinline{DO} (и двух вспомогательных функций, определённых ранее) следующим
образом:
\begin{myverb}
(do ((p (next-prime 0) (next-prime (1+ p))))
((> p 19))
(format t "~d " p))
\end{myverb}
Теперь вы готовы к написанию кода макроса, который будет выполнять необходимое
преобразование.
\section{Макропараметры}
Так как аргументы, передаваемые в макрос, являются объектами Lisp, представляющими
исходный код вызова макроса, первым шагом любого макроса является извлечение тех частей
этих объектов, которые нужны для вычисления раскрытия. Для макросов, которые просто
подставляют свои аргументы напрямую в шаблон, этот шаг тривиален: подходит простое
определение правильных параметров для захвата нужных аргументов.
Но, кажется, подобного подхода недостаточно для \lstinline{do-primes}. Первый аргумент вызова
\lstinline{do-primes} является списком, содержащим имя переменной цикла, \lstinline{p}; нижнюю
границу, 0; верхнюю границу, 19. Но если вы посмотрите на раскрытие, список как целое
не встречается в нем: эти три элемента разделены и вставлены в различные места.
Вы можете определить \lstinline{do-primes} с двумя параметрами, первый для захвата этого списка
и параметр \lstinline!&rest! для захвата форм тела цикла, а затем разобрать первый список
вручную подобным образом:
\begin{myverb}
(defmacro do-primes (var-and-range &rest body)
(let ((var (first var-and-range))
(start (second var-and-range))
(end (third var-and-range)))
`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ,end))
,@body)))
\end{myverb}
Очень скоро я объясню, как тело макроса генерирует правильное раскрытие; сейчас же вам
следует отметить, что переменные \lstinline{var}, \lstinline{start} и \lstinline{end} каждая содержит
значение, извлечённое из \lstinline{var-and-range}, и эти значения затем подставляются в
выражение квазицитирования, генерирующее раскрытие \lstinline{do-primes}.
Однако вам не нужно разбирать \lstinline{var-and-range} вручную, так как список параметров
макроса является так называемым списком \textit{деструктурируемых}
параметров. Деструктурирование, как и говорит название, осуществляет разбор некоторой
структуры, в нашем случае списочной структуры форм, переданных макросу.
Внутри списка деструктурируемых параметров простое имя параметра может быть заменено
вложенным списком параметров. Параметры в таком списке будут получать свои значения из
элементов выражения, которое было бы связано с параметром, заменённым этим
списком. Например, вы можете заменить \lstinline{var-and-range} списком \lstinline{(var start end)},
и три элемента списка будут автоматически деструктурированы в эти три параметра.
Другой особенностью списка параметров макросов является то, что вы можете использовать
\lstinline!&body! как синоним \lstinline!&rest!. Семантически \lstinline!&body! и
\lstinline!&rest! эквиваленты, но множество сред разработки будет использовать факт
наличия параметра \lstinline!&body! для изменения того, как они будут выравнивать код
использования макроса, поэтому обычно параметры \lstinline!&body! применяются для захвата
списка форм, которые составляют тело макроса.
Таким образом, вы можете улучшить определение макроса \lstinline{do-primes} и дать подсказку
(как людям, читающим ваш код, так и вашим инструментам разработки) о его предназначении:
\begin{myverb}
(defmacro do-primes ((var start end) &body body)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ,end))
,@body))
\end{myverb}
В~стремлении к краткости список деструктурируемых параметров также предоставляет вам
автоматическую проверку ошибок: при определении таким образом \lstinline{do-primes} Lisp будет
способен определять вызовы, в которых первый аргумент не является трёхэлементным списком,
и выдавать вам разумные сообщения об ошибках (как когда вы вызываете функцию со слишком
малым или, наоборот, слишком большим числом аргументов). Также среды разработки, такие
как SLIME, указывающие вам, какие аргументы ожидаются, как только вы напечатаете имя
функции или макроса, при использовании вами списка деструктурируемых параметров будут
способны более конкретно указать синтаксис вызова макроса. С исходным определением SLIME
будет подсказывать вам, что \lstinline{do-primes} вызывается подобным образом:
\begin{myverb}
(do-primes var-and-range &rest body)
\end{myverb}
С новым же описанием она сможет указать вам, что вызов должен выглядеть следующим образом:
\begin{myverb}
(do-primes (var start end) &body body)
\end{myverb}
Списки деструктурируемых параметров могут содержать параметры \lstinline!&optional!,
\lstinline!&key! и \lstinline!&rest!, а также вложенные деструктурируемые списки. Однако
все эти возможности не нужны вам для написания \lstinline{do-primes}.
\section{Генерация раскрытия}
Так как \lstinline{do-primes} является довольно простым макросом после деструктурирования
аргументов, всё что вам остаётся сделать,~-- это подставить их в шаблон для получения
раскрытия.
Для простых макросов вроде \lstinline{do-primes} лучшим вариантом является использование
специального синтаксиса квазицитирования. Коротко говоря, выражения квазицитирования
подобны выражениям цитирования, за исключением того, что вы можете <<раскавычить>>
определённые подвыражения, предваряя их запятой, за которой, возможно, следует знак <<at>>
(\lstinline!@!). Без этого знака <<at>> запятая вызывает включение как есть значения
следующего за ней подвыражения. Со знаком <<at>> значение, которое должно быть списком,
<<вклеивается>> в окружающий список.
Другой пригодный способ~-- думать о синтаксисе квазицитирования как об очень кратком
способе написания кода, генерирующего списки. Такое представление о нем имеет
преимущество, поскольку является очень близким к тому, что на самом деле происходит <<под
капотом>>: когда процедура чтения считывает выражение квазицитирования, она преобразует
его в код, который генерирует соответствующую списковую структуру. Например,
\lstinline!`(,a b)!, вероятно, будет прочитано как \lstinline!(list a 'b)!. Стандарт языка
не указывает, какой в точности код процедура чтения должна выдавать, пока она генерирует
правильные списковые структуры.
Таб.~\ref{table:08-1} показывает некоторые примеры выражений квазицитирования вместе с
эквивалентным создающим списки кодом, а также результаты, которые вы получите при
вычислении как выражений квазицитирования, так и эквивалентного
кода\footnote{\lstinline{APPEND}, который я ранее не упоминал, является функцией, которая
получает произвольное число аргументов-списков и возвращает в качестве результата
единственный список, полученный склейкой их вместе.}\hspace{\footnotenegspace}:
\begin{table}[h]
\begin{tabular}{|m{32mm}|m{60mm}|m{30mm}|}
\hline{}
Синтаксис квазицитирования & Эквивалентный код, создающий списки & Результат \\
\hline{}
\lstinline!`(a (+ 1 2) c)! & \lstinline!(list 'a '(+ 1 2) 'c)! & \lstinline!(a (+ 1 2) c)! \\
\lstinline!`(a ,(+ 1 2) c)! & \lstinline!(list 'a (+ 1 2) 'c)! & \lstinline!(a 3 c)! \\
\lstinline!`(a (list 1 2) c)! & \lstinline!(list 'a '(list 1 2) 'c)! & \lstinline!(a (list 1 2) c)! \\
\lstinline!`(a ,(list 1 2) c)! & \lstinline!(list 'a (list 1 2) 'c)! & \lstinline!(a (1 2) c)! \\
\lstinline!`(a ,@(list 1 2) c)! & \lstinline!(append (list 'a) (list 1 2) (list 'c))! & \lstinline!(a 1 2 c)! \\
\hline
\end{tabular}
\caption{Примеры квазицитирования}
\label{table:08-1}
\end{table}
Важно заметить, что нотация квазицитирования является просто удобством. Но это большое
удобство. Для оценки того, насколько оно велико, сравните версию \lstinline{do-primes} с
квазицитированием со следующей версией, которая явно использует создающий списки код:
\begin{myverb}
(defmacro do-primes-a ((var start end) &body body)
(append '(do)
(list (list (list var
(list 'next-prime start)
(list 'next-prime (list '1+ var)))))
(list (list (list '> var end)))
body))
\end{myverb}
Как вы очень скоро увидите, текущая реализация \lstinline{do-primes} не обрабатывает корректно
некоторые граничные случаи. Но первое, что вы должны проверить,~-- это то, что она, по
крайней мере, работает для исходного примера. Вы можете сделать это двумя
способами. Во-первых, вы можете косвенно протестировать свою реализацию, прос\-то
воспользовавшись ею (подразумевая, что если итоговое поведение корректно, то и раскрытие
также корректно). Например, вы можете напечатать исходный пример использования
\lstinline{do-primes} в REPL и увидеть, что он и в самом деле напечатает правильную
последовательность простых чисел.
\begin{myverb}
CL-USER> (do-primes (p 0 19) (format t "~d " p))
2 3 5 7 11 13 17 19
NIL
\end{myverb}
Или же вы можете проверить макрос напрямую, посмотрев на раскрытие определённого
вызова. Функция \lstinline{MACROEXPAND-1} получает любое выражение Lisp в качестве аргумента
и возвращает результат осуществления одного шага раскрытия макроса\footnote{Другая
функция, \lstinline{MACROEXPAND}, продолжает раскрытие результата, пока первый элемент
получаемого раскрытия является именем макроса. Однако это часто показывает вам гораздо
более низкоуровневое представление о том, что делает код, чем вам нужно, так как базовые
структуры контроля, такие как \lstinline{DO}, также реализованы в виде макросов. Другими
словами, в то время как в учебных целях может быть полезно посмотреть, во что в конечном
счёте раскрывается ваш макрос, это не очень полезно для просмотра того, во что
раскрывается именно ваш макрос.}\hspace{\footnotenegspace}. Так как \lstinline{MACROEXPAND-1} является функцией, для
дословной передачи ей формы макроса вы должны зацитировать эту форму. Теперь вы можете
воспользоваться \lstinline{MACROEXPAND-1} для просмотра раскрытия предыдущего
вызова\footnote{Если все раскрытие макроса отображается в одну строку, возможной причиной
является то, что переменная \lstinline{ *PRINT-PRETTY* } установлена в \lstinline{NIL}. Если
это так, вычисление \lstinline{(setf *print-pretty* t)} должно сделать раскрытия макросов
более лёгкими для чтения.}\hspace{\footnotenegspace}.
\begin{myverb}
CL-USER> (macroexpand-1 '(do-primes (p 0 19) (format t "~d " p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))
((> P 19))
(FORMAT T "~d " P))
T
\end{myverb}
Также, для большего удобства, в SLIME вы можете проверить раскрытие макроса, поместив
курсор на открывающую скобку формы макроса в вашем исходном коде и набрав C-c RET для
вызова функции Emacs \lstinline{slime-macroexpand-1}, которая передаст форму макроса в
\lstinline{MACROEXPAND-1} и напечатает результат во временном буфере.
Теперь вы можете видеть, что результат раскрытия макроса совпадает с исходным (написанным
вручную) раскрытием, и поэтому кажется, что \lstinline{do-primes} работает.
\section{Устранение протечек}
В~своём эссе <<Закон дырявых абстракций>> Джоэл Спольски придумал термин <<дырявой
абстракции>> для описания такой абстракции, через которую <<протекают>> детали,
абстрагирование от которых предполагается. Так как написание макроса~-- это способ
создания абстракции, вам следует убедиться, что ваш макрос излишне не
<<протекает>>\pclfootnote{Этот закон, описанный в книге Джоэла Спольски <<Джоэл о
программировании>>, доступен также по адресу
\url{http://www.joelonsoftware.com/articles/LeakyAbstractions.html}. Точка зрения
Спольски, выраженная в эссе, заключается в том, что все абстракции содержат <<протечки>> в той
или иной степени, то есть не существует идеальных абстракций. Но это не значит, что вы
должны допускать <<протечки>>, которые легко устранить.}.
Как оказывается, внутренние детали реализации могут <<протекать>> через макросы тремя
способами. К счастью, довольно легко сказать, имеет ли место одна из трёх этих
возможностей, и устранить их.
Текущее определение страдает от одной из трёх возможных <<протечек>> макросов, а именно оно
вычисляет подформу \lstinline{end} слишком много раз. Предположим, что вы вызвали
\lstinline{do-primes} с таким выражением, как \lstinline{(random 100)}, на месте параметра
\lstinline{end} вместо использования числового литерала, такого как~19.
\begin{myverb}
(do-primes (p 0 (random 100))
(format t "~d " p))
\end{myverb}
Предполагаемым поведением здесь является итерирование по простым числам от нуля до
какого-то случайного простого числа, возвращённого \lstinline{(random 100)}. Однако это не то,
что делает текущая реализация, как это показывает \lstinline{MACROEXPAND-1}.
\begin{myverb}
CL-USER> (macroexpand-1 '(do-primes (p 0 (random 100)) (format t "~d " p)))
(DO ((P (NEXT-PRIME 0) (NEXT-PRIME (1+ P))))
((> P (RANDOM 100)))
(FORMAT T "~d " P))
T
\end{myverb}
При запуске кода раскрытия \lstinline{RANDOM} будет вызываться при каждой проверке условия
окончания цикла. Таким образом, вместо итерирования, пока \lstinline{p} не станет больше, чем
изначально выбранное случайное число, этот цикл будет осуществляться, пока не случится, что
выбранное в очередной раз случайное число окажется меньше текущего значения \lstinline{p}. Хотя
общее число итераций по-прежнему случайно, оно будет подчиняться вероятностному
распределению, отличному от равномерного распределения результатов \lstinline{RANDOM}.
Это является <<протечкой>> абстракции, так как для корректного использования макроса его
пользователь должен быть осведомлён о том, что форма \lstinline{end} будет вычислять более
одного раза. Одним из способов устранения этой <<протечки>> является простое
специфицирование её как поведения \lstinline{do-primes}. Но это не достаточно
удовлетворительно: при реализации макросов вам следует пытаться соблюдать <<правило
наименьшего удивления>>. К тому же программисты обычно ожидают, что формы, которые они
передают макросам, будут вычисляться не большее число раз, чем это действительно
необходимо\footnote{Конечно, для определённых форм, таких как формы тела цикла
\lstinline{do-primes}, предполагается именно вычисление более одного раза.}\hspace{\footnotenegspace}. Более того, так
как \lstinline{do-primes} построена на основе модели стандартных макросов \lstinline{DOTIMES} и
\lstinline{DOLIST}, которые вычисляют однократно все свои формы, кроме форм тела, то
большинство программистов будет ожидать от \lstinline{do-primes} подобного поведения.
Вы можете исправить множественное вычисление достаточно легко: вам просто следует
сгенерировать код, который вычисляет \lstinline{end} однократно и сохраняет результат в
переменную для дальнейшего использования. Вспомним, что в цикле \lstinline{DO} переменные с
формой инициализации и без формы вычисления последующих значений не изменяются от итерации
к итерации. Поэтому вы можете исправить проблему множественных вычислений следующим
определением:
\begin{myverb}
(defmacro do-primes ((var start end) &body body)
`(do ((ending-value ,end)
(,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ending-value))
,@body))
\end{myverb}
К сожалению, данное исправление вводит две новые <<протечки>> в предоставляемую нашим
макросом абстракцию.
Одна из этих <<протечек>> подобна проблеме множественных вычислений, которую мы только что
исправили. Так как формы инициализации переменных цикла \lstinline{DO} вычисляются в том
порядке, в каком переменные определены, то когда раскрытие макроса вычисляется, выражение,
переданное как \lstinline{end}, будет вычислено перед выражением, переданным как
\lstinline{start}, то есть в обратном порядке от того, как они идут в вызове макроса. Эта
<<протечка>> не вызывает никаких проблем, пока \lstinline{start} и \lstinline{end}
являются литералами вроде 0~и~19. Но если они являются формами, которые могут иметь
побочные эффекты, вычисление их в неправильном порядке снова нарушает <<правило наименьшего
удивления>>.
Эта <<протечка>> устраняется тривиально путём изменения порядка определения двух переменных.
\begin{myverb}
(defmacro do-primes ((var start end) &body body)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(ending-value ,end))
((> ,var ending-value))
,@body))
\end{myverb}
Последняя <<протечка>>, которую нам нужно устранить, была создана использованием имени
переменной \lstinline{ending-value}. Проблема заключается в том, что имя, которое должно быть
полностью внутренней деталью реализации макроса, может вступить во взаимодействие с кодом,
переданным макросу, или с контекстом, в котором макрос вызывается. Следующий, кажущийся
вполне допустимым, вызов \lstinline{do-primes} не работает корректно из-за данной <<протечки>>:
\begin{myverb}
(do-primes (ending-value 0 10)
(print ending-value))
\end{myverb}
То же касается и следующего вызова:
\begin{myverb}
(let ((ending-value 0))
(do-primes (p 0 10)
(incf ending-value p))
ending-value)
\end{myverb}
И снова \lstinline{MACROEXPAND-1} может вам показать, в чем проблема. Первый вызов
раскрывается в следующее:
\begin{myverb}
(do ((ending-value (next-prime 0) (next-prime (1+ ending-value)))
(ending-value 10))
((> ending-value ending-value))
(print ending-value))
\end{myverb}
Некоторые реализации Lisp могут отвергуть такой код из-за того, что \lstinline{ending-value}
используется дважды в качестве имён переменных одного и того-же цикла \lstinline{DO}. Если же
этого не произойдёт, то код зациклится, так как \lstinline{ending-value} никогда не станет
больше себя самого.
Второй проблемный вызов раскрывается следующим образом:
\begin{myverb}
(let ((ending-value 0))
(do ((p (next-prime 0) (next-prime (1+ p)))
(ending-value 10))
((> p ending-value))
(incf ending-value p))
ending-value)
\end{myverb}
В~этом случае сгенерированный код полностью допустим, но его поведение совсем не то, что
нужно вам. Так как привязка ending-value, установленная с помощью \lstinline{LET}, снаружи
цикла перекрывается переменной с таким же именем внутри \lstinline{DO}, то форма
\lstinline{(incf ending-value p)} увеличивает переменную цикла \lstinline{ending-value} вместо внешней
переменной с таким же именем, создавая другой вечный цикл\footnote{Может быть не очень
очевидным, что этот цикл обязательно бесконечен, учитывая неравномерное распределение
простых чисел. Начальной точкой доказательства, что он на самом деле бесконечен,
является постулат Бертрана, который говорит, что для любого \lstinline{n > 1} существует
простое число \lstinline{p}~-- такое, что \lstinline!n < p < 2n!. Отсюда вы можете доказать, что
для любого простого числа \lstinline{P}, меньшего, чем сумма предыдущих простых чисел,
следующее простое число \lstinline{P'} также меньше, чем исходная сумма плюс \lstinline{P}.}\hspace{\footnotenegspace}.
Очевидно, что то, что нам нужно для устранения этой <<протечки>>,~-- это символ, который
никогда не будет использоваться снаружи кода, сгенерированного макросом. Вы можете
попытаться использовать действительно маловероятный символ, но это все равно не даст вам
никаких гарантий. Вы можете также защитить себя в некоторой степени путём использования
пакетов, описанных в главе~\ref{ch:21}. Но существует лучшее решение.
Функция \lstinline{GENSYM} возвращает уникальный символ при каждом своём вызове. Такой символ
никогда до этого не был прочитан процедурой чтения Lisp и, так как он не интернирован
(isn't interned) ни в один пакет, никогда не будет прочитан ею. Поэтому вместо
использования литеральных имён вроде \lstinline{ending-value} вы можете генерировать новый
символ при каждом раскрытии \lstinline{do-primes}.
\begin{myverb}
(defmacro do-primes ((var start end) &body body)
(let ((ending-value-name (gensym)))
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body)))
\end{myverb}
Обратите внимание, что код, вызывающий \lstinline{GENSYM}, не является частью раскрытия; он
запускается как часть процедуры раскрытия макроса и поэтому создаёт новый символ при
каждом раскрытии макроса. Это может казаться несколько странным сначала:
\lstinline{ending-value-name} является переменной, чьё значение является именем другой
переменной. Но на самом деле тут нет никаких отличий от параметра \lstinline{var}, чьё значение
также является именем переменной. Единственная разница состоит в том, что значение
\lstinline{var} было создано процедурой чтения, когда форма макроса была прочитана, а значение
\lstinline{ending-value-name} было сгенерированно программно при запуске кода макроса.
С таким определением две ранее проблемные формы раскрываются в код, который работает так,
как вам нужно. Первая форма:
\begin{myverb}
(do-primes (ending-value 0 10)
(print ending-value))
\end{myverb}
\noindent{}раскрывается в следующее:
\begin{myverb}
(do ((ending-value (next-prime 0) (next-prime (1+ ending-value)))
(#:g2141 10))
((> ending-value #:g2141))
(print ending-value))
\end{myverb}
Теперь переменная, используемая для хранения конечного значения, является сгенерированным
функцией \lstinline{gensym} символом, \lstinline!#:g2141!. Имя идентификатора, \textit{G2141},
было сгенерировано с помощью \lstinline{GENSYM}, но важно не это; важно то, что идентификатор
хранит значение объекта. Сгенерированные таким образом символы печатаются в обычном
синтаксисе для неинтернированных символов: с начальным \lstinline!#:!.
Вторая ранее проблемная форма
\begin{myverb}
(let ((ending-value 0))
(do-primes (p 0 10)
(incf ending-value p))
ending-value)
\end{myverb}
\noindent{}после замены \lstinline{do-primes} его раскрытием будет выглядеть подобным образом:
\begin{myverb}
(let ((ending-value 0))
(do ((p (next-prime 0) (next-prime (1+ p)))
(#:g2140 10))
((> p #:g2140))
(incf ending-value p))
ending-value)
\end{myverb}
И снова тут нет никакой <<протечки>>, так как переменная \lstinline{ending-value}, связанная
окружающей цикл \lstinline{do-primes} формой \lstinline{LET}, больше не перекрывается никакими
переменными, вводимыми в коде раскрытия.
Не все литеральные имена, используемые в раскрытии макросов, обязательно вызовут проблему;
когда вы приобретёте больше опыта работы с различными свя\-зы\-ваю\-щи\-ми формами, вы сможете
определять, приведёт ли использование данного имени в определённом месте к <<протечке>> в
предоставляемой макросом абстракции. Но нет никаких реальных проблем в использовании
сгенерированных имён везде для уверенности.
Этим исправлением мы устранили все <<протечки>> в реализации \lstinline{do-primes}. После
получения некоторого опыта в написании макросов вы научитесь писать макросы с заранее
устранёнными <<протечками>> такого рода. На самом деле это довольно просто, если вы будете
следовать следующим правилам:
\begin{itemize}
\item Если только нет определённой причины сделать иначе, включайте все подформы в
раскрытие на такие позиции, чтобы они выполнялись в том же порядке, в каком они идут в
вызове макроса.
\item Если только нет определённой причины сделать иначе, убедитесь, что все подформы
вычисляются лишь единожды, путём создания переменных в раскрытии, для хранения значений
вычисления форм аргументов и последующего использования этих переменных везде в
раскрытии, где нужны значения этих форм.
\item Используйте \lstinline{GENSYM} во время раскрытия макросов для создания имён
переменных, применяемых в раскрытии.
\end{itemize}
\section{Макросы, создающие макросы}
Конечно же, нет никаких причин, по которым вы должны получать преимущества от
использования макросов только при написании функций. Задачей макросов является
абстрагирование общих синтаксических образцов, а некоторые образцы появляются снова и
снова и при написании макросов, поэтому и тут можно получить преимущества от
абстрагирования.
На самом деле вы уже видели один такой образец: многие макросы, как и последняя версия
\lstinline{do-primes}, начинаются с \lstinline{LET}, который вводит несколько переменных,
содержащих сгенерированные символы для использовании в раскрытии макроса. Так как это
общий образец, почему бы нам не абстрагировать его с помощью его собственного макроса?
В~этом разделе вы напишете макрос \lstinline{with-gensyms}, который делает именно это. Другими
словами, вы напишете макрос, создающий макрос: макрос, который генерирует код, который
генерирует код. В~то время как сложные макросы, создающие макросы, могут слегка сбивать с
толку, пока вы не привыкнете к лёгкому умозрительному обращению с различными уровнями
кода, \lstinline{with-gensyms} довольно прямолинеен и послужит полезным и в то же время не
требующим непомерных умственных усилий упражнением.
Предположим, вы хотите иметь возможность написать подобное:
\begin{myverb}
(defmacro do-primes ((var start end) &body body)
(with-gensyms (ending-value-name)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body)))
\end{myverb}
\noindent{}и получить \lstinline{do-primes}, эквивалентный его предыдущей версии. Другими словами,
\lstinline{with-gensyms} должен раскрываться в \lstinline{LET}, которая связывает каждую
перечисленную переменную, \lstinline{ending-value-name} в данном случае, со сгенерированным
символом. Достаточно просто написать это с помощью простого шаблона-квазитирования.
\begin{myverb}
(defmacro with-gensyms ((&rest names) &body body)
`(let ,(loop for n in names collect `(,n (gensym)))
,@body))
\end{myverb}
Обратите внимание, как мы можем использовать запятую для подстановки значения выражения
\lstinline{LOOP}. Этот цикл генерирует список связывающих форм, каждая из которых состоит из
списка, содержащего одно из переданных \lstinline{with-gensyms} имён, а также литеральный код
\lstinline{(gensym)}. Вы можете проверить, какой код сгенерирует выражение \lstinline{LOOP} в
REPL, заменив \lstinline{names} списком символов.
\begin{myverb}
CL-USER> (loop for n in '(a b c) collect `(,n (gensym)))
((A (GENSYM)) (B (GENSYM)) (C (GENSYM)))
\end{myverb}
После списка связывающих форм в качестве тела \lstinline{LET} вклеивается аргумент
\lstinline{body} \lstinline{with-gensyms}. Таким образом, из кода, который вы оборачиваете в
\lstinline{with-gensyms}, вы можете ссылаться на любое из имён переменных из списка переменных,
переданного \lstinline{with-gensyms}.
Если вы воспользуетесь macro-expand для формы \lstinline{with-gensyms} в новом определении
\lstinline{do-primes}, то получите подобное:
\begin{myverb}
(let ((ending-value-name (gensym)))
`(do ((,var (next-prime ,start) (next-prime (1+ ,var)))
(,ending-value-name ,end))
((> ,var ,ending-value-name))
,@body))
\end{myverb}
Выглядит неплохо. Хотя этот макрос довольно прост, очень важно ясно понимать то, когда
различные макросы раскрываются: когда вы компилируете \lstinline{DEFMACRO} \lstinline{do-primes},
форма \lstinline{with-gensyms} раскрывается в код, который вы только что видели. Таким образом,
скомпилированная версия \lstinline{do-primes} в точности такая же, как если бы вы написали
внешний \lstinline{LET} вручную. Когда вы компилируете функцию, которая использует
\lstinline{do-primes}, то для раскрытия \lstinline{do-primes} запускается код,
сгенерированный \lstinline{with-gensyms}, но сам \lstinline{with-gensyms} при компиляции формы
\lstinline{do-primes} не нужен, так как он уже был раскрыт при компиляции \lstinline{do-primes}.
\section{Другой классический макрос, создающий макросы: ONCE-ONLY}
Другим классическим макросом, создающим макросы, является \lstinline{once-only}, который
используется для генерации кода, вычисляющего определённые аргументы макроса только
единожды и в определённом порядке. Используя \lstinline{once-only}, вы можете написать
\lstinline{do-primes} почти таким же простым способом, как исходную <<протекающую>> версию,
следующим образом:
\begin{myverb}
(defmacro do-primes ((var start end) &body body)
(once-only (start end)
`(do ((,var (next-prime ,start) (next-prime (1+ ,var))))
((> ,var ,end))
,@body)))
\end{myverb}
Однако реализация \lstinline{once-only} несколько запутана для обычного пошагового
объяснения, так как зависит от множества уровней квазицитирования и <<раскавычивания>>. Если
вы действительно хотите попрактиковаться в понимании макросов, вы можете попытаться
разобраться, как он работает. Макрос выглядит следующим образом:
\begin{myverb}
(defmacro once-only ((&rest names) &body body)
(let ((gensyms (loop for n in names collect (gensym))))
`(let (,@(loop for g in gensyms collect `(,g (gensym))))
`(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
,@body)))))
\end{myverb}
\section{Не только простые макросы}
Конечно, я могу расказать о макросах намного больше. Все макросы, которые вы до сих пор
видели, были довольно простыми примерами, избавляющими вас от небольшого количества работы
по набору текста, но не предоставляющими радикально новых способов выражения мыслей. В
последующих главах вы увидите примеры макросов, позволяющих вам выражать мысли способами,
практически не возможными без макросов. И вы начнёте прямо со следующей главы, в которой
создадите простой, но эффективный фреймворк для модульного тестирования.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End: