Часто бывает полезно не выписывать вычисляемое выражение вручную, а сформировать его с помощью программы. Эта идея автоматического динамического программирования особенно хорошо реализуется в Лиспе, поскольку программа в этом языке также представляется в виде списка. При этом вычисление такого выражения или его части при необходимости можно предотвратить блокировкой ('), например, с целью преобразования выражения. Для вычисления же сформированного выражения программист всегда может вызвать интерпретатор (eval). Предусмотренные в Лиспе специальные способы обработки аргументов открывают возможность для комбинирования различных методов.
Перечисленные возможности можно использовать в Лиспе и без специальных средств. Однако наиболее естественно программное формирование выражений осуществляется с помощью специальных макросов. Внешне макросы определяются и используются так же, как функции, отличается лишь способ их вычисления. Вычисляя вызов макроса, сначала из его аргументов строится форма, задаваемая определением макроса. В результате вызова возвращается значение этой формы, а не сама форма, как было бы при вычислении функций. Таким образом, макрос вычисляется как бы в два этапа.
Макросы представляют собой абстрактный механизм, с помощью которого можно определить формирование и вычисление произвольной (вычислимой) формы или целой программы.
Макросы дают возможность расширять синтаксис и семантику Лиспа и использовать новые подходящие для решаемой задачи формы предложений. Абстракции такого характера называют абстракциями проблемной области, а определяемое ими расширение языка Лисп - встроенным языком.
Макросы - это мощный рабочий инструмент программирования. Они дают возможность писать компактные, ориентированные на задачу программы, которые автоматически преобразуются в более сложный, но более близкий машине эффективный код. Однако с их использованием тоже связаны свои трудности и опасности. Создаваемые в процессе вычислений формы часто трудно увидеть непосредственно из определения макроса или из формы его вызова.
Синтаксис определения макроса выглядит так же, как синтаксис используемой при определении функций формы defmethod:
(имя-класса defmacro имя-макроса список-аргументов тело)
Вызов макроса совпадает по форме с вызовом функции, но его вычисление отличается от вычисления вызова функции. Первое отличие состоит в том, что в макросе не вычисляются аргументы. Тело макроса вычисляется с аргументами в том виде, как они записаны. Методы всегда предварительно вычисляют свои аргументы, а если есть необходимость чтобы аргумент не вычислялся, то такие формы определяются с помощью макроса.
Второе отличие макроса от функции связано со способом вычисления тела макроса. Вычисление вызова макроса состоит из двух пошаговых этапов. На первом этапе осуществляется вычисление тела определения с аргументами из вызова таким же образом, как и для функции. Этот этап называют этапом расширения или раскрытия макроса, поскольку возникающая форма, как правило, больше и сложнее исходной формы вызова. Часто говорят и о трансляции макросов, поскольку на этапе расширения макровызов транслируется в некоторое вычислимое выражение. На втором этапе вычисляется полученная из вызова раскрытая форма, значение которой возвращается в качестве значения всего макровызова.
Определим, например, макрос setqq, который действует наподобие setq, но блокирует вычисление и второго своего аргумента:
>(nil setq список) nil >(nil defmacro setqq (x y) `(nil setq ,x ',y)) (macro (x y) `(nil setq ,x ',y)) >(nil setqq список (a b c)) ; setqq не вычисляет свои аргументы (a b c) >список (a b c)
После этапа расширения макровызова значением тела макроса было
(nil setq список '(a b c))
На втором этапе эта программно созданная форма вычисляется обычным образом и её значение возвращается в качестве значения вызова макроса. В этом случае у возникшей формы есть побочный эффект.
Итак, макрос - это форма, которая во время вычисления заменяется на новую, обычно более сложную форму, которая затем вычисляется обычным образом.
Возникающее во время расширения макроса новое выражение может вновь содержать макровызов, возможно рекурсивный. Содержащиеся в расширении макровызовы приводят на втором этапе вычислений к новым макровычислениям. С помощью рекурсивного макроса можно осуществить расширение, динамически зависящее от параметров, и продолжающееся макровычисление. Например, копирование верхнего уровня списка можно было бы определить следующим рекурсивным макросом:
>(nil defmacro copy ()) (macro nil) >('cons defmacro copy () (nil list nil 'cons ; вызов cons в качестве расширения `',(this first) (nil list `',(this rest) 'copy))) (macro nil (nil list nil 'cons `',(this first) (nil list `',(this rest) 'copy))) >('(a b c) copy) (a b c)
В качестве первого расширения формируется выражение, содержащее новый макровызов:
(nil cons 'a ('(b c) copy))
Таким образом, расширение на втором этапе вычислений приводит к рекурсивному макровызову. Рекурсия заканчивается на вызове (nil copy), значением расширения которого является nil.
Как при определении и вызове функции, в случае макросов в лямбда-списке можно использовать то же ключевое слово &rest.
Параметр &rest используется для указания на заранее неопределённое количество аргументов. Этот механизм применяется и для определения форм, не все аргументы которых нужно вычислять или в которых аргументы желательно обрабатывать нестандартным образом. Например, в обычной форме cond предикаты вычисляются лишь до тех пор, пока не будет получено первое значение, отличное от nil и false. Для определения таких форм функции не подходят.
Зададим простую форму cond с помощью следующего рекурсивного макроса:
(nil defmacro cond (&rest ветви) (nil if (nil consp ветви) (nil let ((ветвь (ветви first))) (nil if (nil eval (ветвь first)) (nil list* nil 'progn (ветвь rest)) (nil list* nil 'cond (ветви rest))))))
При раскрытии макроса обычно используется большое количество вложенных друг в друга вызовов функций cons, first, rest, list, + и других. Поэтому при построении расширения можно легко ошибиться, а само макроопределение становится менее прозрачным. Для облегчения написания макросов в Лиспе принят специальный механизм блокировки вычислений, который называют обратной блокировкой и который помечается в отличие от обычной блокировки (') наклонённым в другую сторону (обратным) апострофом ` (back quote).
Внутри обратно блокированного выражения можно по желанию локально отменять блокировку вычислений, иными словами внутри некоторого подвыражения опять осуществлять вычисления. Отсюда происходит и название обратной блокировки. Отмена блокировки помечается запятой , перед каждым предназначенным для вычисления подвыражением. Запятая даёт возможность на время переключиться в нормальное состояние вычислений. Пример:
>'(не вычисляется (3 + 4)) ; обыкновенный ' не отменяется (не вычисляется (3 + 4)) >`(можно вычислить (3 + 4)) ; ` действует как обычная блокировка (можно вычислить (3 + 4)) >`(желательно вычислить ,(3 + 4)) ; , перед выражением приводит к его вычислению (желательно вычислить 7)
В расширяемом выражении при обратной блокировке выражения с предваряющей запятой заменяются на их значения. Использование предваряющей запятой можно назвать замещающей отменой блокировки. Кроме запятой можно использовать запятую вместе со знаком @ (at-sign) присоединения подвыражения. Выражение, перед которым стоит признак присоединения ,@, вычисляется обычным образом, но полученное выражение присоединяется к конечному выражению таким же образом, как это делает функция +. Так внешние скобки списочного значения пропадут, а элементы станут элементами списка верхнего уровня. Такую форму отмены блокировки называют присоединяющей. Приведём пример:
>(nil setq x '(новые элементы)) nil >`(включить ,x в список) ; замещающая отмена (включить (новые элементы) в список) >`(включить ,@x в список) ; присоединяющая отмена (включить новые элементы в список)
Блокировка вычислений и знаки отмены определены в Лиспе как макросы чтения. Изучить способы формирования выражений можно, задавая интерпретатору выражения, в которых внутри ' содержится обратная блокировка. Например:
>'`(a (b ,c)) `(a (b ,c))
В качестве значения получается иерархия вызовов системных функций, вычислению значения которой препятствует апостроф.
Обратная блокировка является удобным средством формирования макроса. С её помощью макрос можно определить в форме, близкой к виду его раскрытого выражения. Например, макрос list можно определить очень просто:
(nil defmacro list (&rest args) (nil if (nil consp args) `(nil cons ,(args first) ,(nil list* nil 'list (args rest)))))
Обратная блокировка даёт возможность определить раскрытое выражение в виде образца, в котором динамически заполняемые формы помечены запятой. Это помогает избежать сложной комбинации вызовов функций cons, list и других.
Обратная блокировка используется не только в макроопределениях. Например, построение результатов для функции writeln часто приводит к использованию вызовов cons, list и других функций. Обратной блокировкой мы вновь приближаемся к окончательному выводимому виду:
>('stream defmethod добавь-и (x y z) (this writeln `(,x ,y и ,z))) (lambda (x y z) (this writeln `(,x ,y и ,z))) >(cout добавь-и 'ниф 'наф 'нуф) (ниф наф и нуф) STREAM:Stdout
добавь-и можно было бы определить и следующим макросом, для которого не нужны апострофы перед аргументами:
>('stream defmacro добавь-и (x y z) `(,this writeln '(,x ,y и ,z))) (macro (x y z) `(,this writeln '(,x ,y и ,z))) >(cout добавь-и ниф наф нуф) (ниф наф и нуф) STREAM:Stdout
Интересный тип макросов образуют макросы с побочным эффектом, или макросы, меняющие определение. Поскольку, изменяя, они одновременно что-то уничтожают, то их ещё называют структура-разрушающими макросами.
С помощью подходящего побочного эффекта часто можно получить более эффективное решение как в макросах, так и в работе со структура-разрушающими функциями. Однако использовать их надо очень внимательно, иначе получаемый эффект станет для программы разрушающим в буквальном смысле этого слова.
Обычно интерпретатор Лиспа расширяет макровызовы при каждом обращении заново, что в некоторых ситуациях может быть слишком неэффективным. Альтернативным решением может стать программирование такого макроса с привлечением побочного эффекта, который заменяет с помощью псевдофункций (setfirst, setrest, и др.) макровызов на его расширение.Такие макросы нет надобности расширять каждый раз, так как при следующим вызове макроса на месте макровызова будет полученное при первом вызове расширение. Например:
>('cons defmacro первый () `(',this first)) (macro nil `(',this first)) >('(a b c) первый) a >('cons defmethod выведи-первый () (cout writeln (this первый))) (lambda nil (cout writeln (this первый)))
По этим определениям при каждом вызове функции выведи-первый производится расширение макроса первый:
>('(a b c) выведи-первый) a STREAM:Stdout
Превратим теперь макрос первый в структуроразрушающий макрос:
('cons defmacro первый (&whole вызов) ((вызов rest) setfirst 'first) вызов)
Расширение макроса происходит теперь только при первом вызове функции выведи-первый, которое в качестве побочного эффекта преобразует форму (this первый) в теле выведи-первый в форму (this first). Вычисления соответствуют ситуации, в которой выведи-первый с самого начала была бы определена в виде:
('cons defmethod выведи-первый () (cout writeln (this first)))
Кроме синтаксического определения форм макросы применяются и для определения новых методов. Мы уже познакомились с основными типами данных Лиспа: числами, символами и списками. Далее мы в качестве простейшего примера применения макросов для работы с типами данных зададим макрос defдоступ, который определяет предназначенную для чтения свойства символа простейшую функцию доступа, именем которой является само это свойство, а аргументом - символ. С помощью такой функции доступа можно читать свойство символа в более простой форме, чем с помощью формы get, а именно
(символ свойство)
; Макрос определения функции доступа ('symbol defmacro defдоступ () `('symbol defmethod ,this () (this get ',this)))
В результате вызова макроса мы в качестве побочного эффекта получим определение нового метода с помощью defmethod.
>('яблоко put 'цвет 'красный) ; присваивание свойства красный >('цвет defдоступ) ; определение с помощью макроса доступа (lambda nil (this get 'цвет)) >('яблоко цвет) ; чтение свойства теперь стало проще красный
С помощью макросов можно легко написать программы, которые формируют другие программы и сразу же вычисляют их.