Перейти к содержанию

Лисп/Макросы

Материал из Викиучебника — открытых книг для открытого мира

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

(defmacro имя-макроса список-аргументов тело-макроса).

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

(defun foo1 (x)
  (list 'exp x))


(defmacro foo2 (x)
  (list 'exp x))

И вычислим их:

> (foo1 1)

(EXP 1)

> (foo2 1)

2.7182817


Видно, что результат вычисления функции - есть новый список, результат вычисления макроса - число. Чтобы увидеть, как прошел первый этап вычисления макроса, существует функция macroexpand, возвращающая два значения: результат раскрытия и t, если аргументом был макрос. Обратите внимание, что аргумент этой функции необходимо квотировать:

> (macroexpand '(foo2 1))

(EXP 1) ;

T


Это очень полезная функция, позволяющая убедиться, что макрос раскрылся в нужное выражение. Если в теле макроса встречаются другие макросы, то раскрытие совершается в несколько этапов. Каждый этап раскрытия макросов можно посмотреть при помощи функции macroexpand-1.

Еще одной особенностью макросов является то, что аргументы переданные в макрос (в отличие от функции) не вычисляются:

> (macroexpand '(foo2 (+ 5 6)))

(EXP (+ 5 6)) ;

T

> (foo1 (+ 5 6))

(EXP 11)


Итак, мы ввели новую синтаксическую конструкцию, которая трансформируется из выражения (foo2 аргумент) в (exp аргумент), и затем вычисляется. В реальности же преобразования могут быть довольно объемными и не такими очевидными. Например, встроенный макрос cond трансформируется в цепочку операторов if:

(cond ((< x 5) 5)
      ((> x 10) 10)
      (t x))
;; ----->
(if (< x 5)
    5
    (if (> x 10)
        10
        x))
> (macroexpand '(cond ((< x 5) 5) ((> x 10) 10) (t x)))

(IF (< X 5) 5 (IF (> X 10) 10 X)) ;

T


В подобных объемных макросах постоянно встречаются функции составления списков, для того что бы избежать перегрузки кода подобными конструкциями, был введен облегчающий чтение кода синтаксис для конструирования макросов. Символ " ` " (backquote, обратная кавычка) используется для квотирования всего выражения почти как " ' " (одиночная кавычка), с тем лишь отличием, что квотированное таким образом выражение, можно частично или полностью вычислить в процессе раскрытия макроса, поставив перед вычисляемыми списками и символами " , " (запятую). Макрос foo2 можно переписать в виде:

;(defmacro foo2 (x)
;  (list 'exp x))
; ------>
(defmacro foo2 (x)
  `(exp ,x))

Сразу бросается в глаза то, что теперь тело макроса выглядит примерно так же, как и то, во что он раскроется. Читабельность кода несколько возросла, отпала необходимость использовать list и quote и присутствует экономия нескольких байт дискового пространства. Кроме того существует еще один очень полезный символ " ,@ " (запятая с at), который позволяет избавиться и от функции cons:

;(defmacro foo3 (x)
;  (cons '+ x))
; ------>
(defmacro foo3 (x)
  `(+ ,@x))
> (macroexpand '(foo3 (1 2 3)))

(+ 1 2 3) ;

T


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

Необходимо помнить, что в Common Lisp макрос раскрывается в некой лексической области, и значения используемых в нем символов определяются этой областью. Из-за этого макрос внутри одних областей может вести себя несколько иначе, чем в других областях, кроме того, макрос сам может экранировать внешние лексические области. Если эти эффекты не нужны, то в макросах необходимо использовать уникальные символы, их можно сгенерировать при помощи функции gensym:

> (gensym)


#:G3102


Теперь их можно использовать в определениях макросов:

(defmacro foo4 (x)
  `(let ((y 5))
     (+ y ,x)))

(defmacro foo5 (x)
  (let ((y (gensym)))
    `(let ((,y 5))
       (+ ,y ,x))))

В приведенном выше примере оба макроса, кажется, делают одно и то же, но макрос foo4 экранирует лексическую область, в которой он будет раскрываться, и если в качестве параметра ему передать y, то, скорее всего, результат будет не тем, который ожидает программист, использующий его. А макрос foo5 избавлен от этого недостатка (это называется соблюдать гигиену, а макросы, соответственно, гигиеническими). Результат соблюдения гигиены налицо:

> (macroexpand '(foo4 y))

(LET ((Y 5)) (+ Y Y)) ;

T

> (macroexpand '(foo5 y))

(LET ((#:G3099 5)) (+ #:G3099 Y)) ;

T


И совсем нетрудно догадаться, что произойдет при вычислении этих макросов:

> (let ((y 1)) (foo4 y))

10

> (let ((y 1)) (foo5 y))

6


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

(defmacro aif (if-c then-c &optional else-c)
  `(let ((it ,if-c))
     (if it ,then-c ,else-c)))

> (aif (+ 5 10) (* it 2) 10)

30


Мы с вами рассмотрели самые азы использования макросов. При помощи них можно повышать выразительность программ. Умело комбинируя их с функциями, строить все более высокие уровни абстракции, превращать Common Lisp в различные предметно-ориентированные языки (DSL - Domain specific language), создавать эффективные компиляторы других языков программирования.