Регистрация | Войти
Lisp — программируемый язык программирования

Макросы

Как работают макросы

Слово macro в информатике обычно означает расширение синтаксиса языка программирования. Оно происходит от слова macro-instruction, которое означало полезную возможность высокоуровневых языков. Одна макро-инструкция "разворачивалась" в несколько обычных инструкций. Эта идея использовалась многократно, например, в препроцессоре Си. Многие языки имеют собственные макро-средства, но ни у одного языка нет таких мощных макросов, как у лиспа. Механизм использования макросов в лиспе прост, хотя и имеет несколько усложняющих особенностей, и изучение макросов лиспа займет некоторое время и потребует практики.1)

Макрос - это обычный кусок лисп-кода, который выполняет некоторые действия над другим куском лисп-кода, приводя его к исполняемому виду. Это может звучать слишком отпугивающе, но вот простой пример. Представьте, что вы хотите иметь вариант функции setq, присваювающий определенное значение не одной, а сразу двум переменным, например, так:

(my-setq x y (+ z 3))

Если z=8, то x и y получат одинаковое значение 11.

Должно быть совершенно очевидно, что мы не сможем это сделать с помощью обычной функции. Мы хотим, чтобы запись (my-setq v1 v2 e) исполнялась как (progn (setq v1 e) (setq v2 e)). Строго говоря, макрос делает это немного иначе, но пока пусть будет так: макрос позволяет нам сделать это, определяя программу, преобразующую входной код (my-setq v1 v2 e) в выходной (progn ... ).

Вот как мы можем определить макрос my-setq:

(defmacro my-setq (v1 v2 e)
  (list 'progn (list 'setq v1 e) (list 'setq v2 e))
)

Это очень похоже на описание аналогичной функции:

(sefun my-setqF (v1 v2 e)
  (list 'progn (list 'setq v1 e) (list 'setq v2 e))
)

Следующий вызов функции (my-setqF 'x 'y '(+ z 3)) вернет (progn (setq x (+ z 3)) (setq y (+ z 3))). Это самая обычная операция, которая возвращает лисп-код, который может быть исполнен. Что делает defmacro? Определение defmacro неявно определяет аналогичную функцию и сообщает компилятору, что если тот встретит my-setq, эта функция будет вызвана с аргументами x, y и (+ z 3). Исходный вызов my-setq будет заменен на код, который выдаст вызванная функция. То есть, макрос "разворачивается" в новый кусок кода.

Вот и все, за исключением ряда обстоятельств. Самое важное обстоятельство - макрос раскрывается во время компиляции. Компилятор осуществляет преобразование кода, но не выполняет его.

Новички часто допускают ошибку такого рода. Представьте, что макрос my-setq выполняет некоторое сложное преобразование с аргументом e до того, как присвоит его значение двум переменным. Пусть это преобразование может быть записано в виде процедуры comtran. Новичок может написать так:

(defmacro my-setq (v1 v2 e)
  (let ((e1 (comtran e)))
    (list 'progn (list 'setq v1 e1) (list 'setq v2 e1))
)
)


(defmacro comtran (exp) ... ) ;; это неверно!

Ошибка заключается в том, что (comtran e) вернет преобразованное значение e, а не код для его преобразования, который должен быть встроен в исполнимый кусок кода, который получится в результате определения (defmacro my-setq ... ). Т.к. на момент раскрытия макроса (при компиляции) значение аргумента e неизвестно, то и само преобразование с помощью макроса comtran выполнить не получится, и такая попытка определить my-setq приведет к ошибке.

(defmacro my-setq (v1 v2 e)
  (let ((e1 (comtran e)))
    (list 'progn (list 'setq v1 e1) (list 'setq v2 e1))
)
)


(defun comtran (exp) ... ) ;; вот теперь правильно

Определив comtran как функцию, вызов (comtran e) вернет код, выполняющий необходимое нам преобразование (аналогично вызову функции my-setqF).

Другое усложнение заключается в том, что некоторые аргументы макро-функции могут быть ключевыми или опционными.

(defmacro foo (x &optional y &key (cxt 'null)) ... )

Далее, при вызове (foo a) параметры будут определены как x=a, y=nil, cxt=null;

при вызове (foo (+ a 1) (- y 1)) – x=(+ a 1), y=(- y 1), cxt=null;

при вызове (foo a b :cxt (zap zip)) – x=a, y=b, cxt=(zap zip).

Переменные получат в качестве значений сами выражения (+ a 1) и (zap zip). Совсем не важно, известно ли значение этих выражений, и могут ли они вообще иметь значения. Макрос волен делать с ними все, что угодно. Вот, например, еще более бесполезный вариант функции setq:

(defmacro setq-reversible (e1 e2 dir)
  (ecase dir
    (:normal (list 'setq e1 e2))
    (:backward (list 'setq e2 e1))
)
)

(setq-reversible e1 e2 dir) ведет себя как (setq e1 e2), если dir=:normal, и как (setq e2 e1), если dir=:backward.

Обратные кавычки

Перед тем, как идти дальше, следует сказать об одном элементе синтаксиса лиспа, который сам по себе не имеет к макросам никакого отношения. Это обратная кавычка. Как было показано выше, основная задача макроса - определить кусок кода, например, такого вида: (list 'progn (list 'setq ... ) ... ). Когда такие выражения становятся более сложными, они одновременно становятся менее читаемыми. С помощью обратной кавычки можно записать это так:

`(progn (setq ,v1 ,e) (setq ,v2 e))

Обратная кавычка, так же как обычная, означает, что в следующем за ней выражении каждое подвыражение квотируется (не вычисляется). Однако, в отличие от обычной кавычки, внутри такого выражения возможно вычисление отдельных подвыражений, которые помечаются запятой.

Вот по сути и все по поводу обратной кавычки. Но есть еще одна связанная с ней полезная возможность: ",@". Например, есть v=(oh boy), то `(zap ,@v ,v) вычислится как (zap oh boy (oh boy)).

Еще один момент. Что будет, если внутри выражения с обратной кавычкой появится еще одна обратная кавычка? При неправильном использовании чтение и запись отдельных элементов внутри вложенного backquote-цитирования окажется невозможным, и этого лучше избегать. Хотя это и возможно, и может даже оказаться очень полезным, рассмотрение этого вопроса выходит за рамки данного туториала.

1)По-русски слово "macro" звучит как "макрос", вероятно, благодаря стараниям небезызвестной корпорации
@2009-2013 lisper.ru