Лисп/Макросы
Макросы в Лиспе - это мощный механизм синтаксической абстракции, позволяющий преобразовывать одни лисповые выражения в другие.
Определяются макросы при помощи макроса defmacro
следующим образом:
(defmacro имя-макроса список-аргументов тело-макроса)
.
Очень похоже на определение функции, но от функций макросы отличаются способом вычисления, которое проходит в два этапа: получение нового выражения (раскрытие макроса) и вычисление этого выражения. Например, определим простенькие макрос и функцию с одинаковыми телами:
(defun foo1 (x)
(list 'exp x))
(defmacro foo2 (x)
(list 'exp x))
И вычислим их:
(EXP 1)
> (foo2 1)
2.7182817
Видно, что результат вычисления функции - есть новый список, результат вычисления макроса - число. Чтобы увидеть, как прошел первый этап вычисления макроса, существует функция macroexpand
, возвращающая два значения: результат раскрытия и t, если аргументом был макрос. Обратите внимание, что аргумент этой функции необходимо квотировать:
(EXP 1) ;
T
Это очень полезная функция, позволяющая убедиться, что макрос раскрылся в нужное выражение. Если в теле макроса встречаются другие макросы, то раскрытие совершается в несколько этапов. Каждый этап раскрытия макросов можно посмотреть при помощи функции macroexpand-1
.
Еще одной особенностью макросов является то, что аргументы переданные в макрос (в отличие от функции) не вычисляются:
(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))
(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))
(+ 1 2 3) ;
T
Этот же синтаксис чисто технически можно использовать и в определениях функций, но при этом могут возникать неприятные побочные эффекты и лучше этого не делать.
Необходимо помнить, что в Common Lisp макрос раскрывается в некой лексической области, и значения используемых в нем символов определяются этой областью. Из-за этого макрос внутри одних областей может вести себя несколько иначе, чем в других областях, кроме того, макрос сам может экранировать внешние лексические области. Если эти эффекты не нужны, то в макросах необходимо использовать уникальные символы, их можно сгенерировать при помощи функции gensym
:
Теперь их можно использовать в определениях макросов:
(defmacro foo4 (x)
`(let ((y 5))
(+ y ,x)))
(defmacro foo5 (x)
(let ((y (gensym)))
`(let ((,y 5))
(+ ,y ,x))))
В приведенном выше примере оба макроса, кажется, делают одно и то же, но макрос foo4
экранирует лексическую область, в которой он будет раскрываться, и если в качестве параметра ему передать y
, то, скорее всего, результат будет не тем, который ожидает программист, использующий его. А макрос foo5
избавлен от этого недостатка (это называется соблюдать гигиену, а макросы, соответственно, гигиеническими). Результат соблюдения гигиены налицо:
(LET ((Y 5)) (+ Y Y)) ;
T
> (macroexpand '(foo5 y))
(LET ((#:G3099 5)) (+ #:G3099 Y)) ;
T
И совсем нетрудно догадаться, что произойдет при вычислении этих макросов:
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), создавать эффективные компиляторы других языков программирования.