Вот игрушечный макроэкспандер, написанный на Racket, который имеет дело с макросами в стиле CL.В процессе написания этой статьи я использовал макросы Racket и другие средства, так что он сам по себе не был загружен.Очевидно, что такое можно сделать, но было бы гораздо сложнее сделать это.
Цель этого состоит исключительно в том, чтобы продемонстрировать, как может работать простой макроэкспандер: он ни в каком смысле не должен быть чем-топодходит для реального использования.
Специальные формы
Прежде всего нам нужно иметь дело со специальными формами.Специальные формы - это вещи, которые имеют магическую семантику.Этот расширитель имеет очень простое представление о том, как они работают:
- специальная форма - составная форма, первый элемент которой является специальным оператором;
- каждый элемент остальной части формыэто либо какая-то особая вещь, которая не раскрывается, либо расширяется нормально, что делается с помощью выражения
expr
в определении; - способ, которым это делается, с помощью довольно бессмысленного сопоставителя паттернов, которыйвероятно, достаточно только потому, что существует небольшое количество специальных форм, о которых экспандер знает.
Итак, вот как определяются специальные формы, и определение трех из них:
(define special-patterns (make-hasheqv))
(define (special-pattern? op)
(and (symbol? op)
(hash-has-key? special-patterns op)))
(define (special-pattern op)
(hash-ref special-patterns op))
(define-syntax-rule (define-special-pattern (op spec ...))
(hash-set! special-patterns 'op '(op spec ...)))
(define-special-pattern (quote thing))
(define-special-pattern (lambda args expr ...))
(define-special-pattern (define thing expr ...))
(define-special-pattern (set! thing expr))
Теперь мы можем спросить, является ли что-то особой формой (специальным шаблоном в коде) и извлечь ее шаблон:
> (special-pattern? 'lambda)
#t
> (special-pattern 'lambda)
'(lambda args expr ...)
Обратите внимание, что такие вещи, как if
, не являются специальными операторами для макроэкспандера, дажехотя на самом деле они особенные: в такой форме, как (if test then else)
, все подчиненные формы должны быть расширены, поэтому у макроэкспандера нет причин знать о них.Это только такие вещи, как lambda
, где некоторые субформы должны быть не расширены, о которых нужно знать макроэкспандеру.
Определения макросов
Макросы являются составными формами, первый элемент которыхраспознается как именование макроса.Для каждого такого макроса есть функция макроэкспандера, которая будет отвечать за расширение формы: этой функции передается вся форма.Существует немного синтаксиса, то есть define-macro
, который оборачивает эту функцию аналогично тому, как defmacro
делает в CL (но нет поддержки &whole
, поддержки деструктуризации arglist или чего-либо еще).
(define macros (make-hasheqv))
(define (macro? op)
(and (symbol? op)
(hash-has-key? macros op)))
(define (macro op)
(hash-ref macros op))
(define-syntax-rule (define-macro (m arg ... . tail) form ...)
(hash-set! macros 'm (lambda (whole)
(apply (lambda (arg ... . tail) form ...)
(rest whole)))))
С помощью этого мы можем определить простой макрос: вот четыре определения для let
.
Прежде всего, это самое элементарное: здесь даже не используется define-macro
но это то, во что она превращается: внешняя функция получает всю форму, а затем вызывает внутреннюю часть, которая не является именем макроса.Затем внутренняя функция кропотливо превращает (let ((x y) ...) ...)
в ((lambda (x ...) ...) y ...)
, что является правильным расширением для let
.(Обратите внимание, что все это не относится к CL * (let (x) ...)
).
(hash-set! macros 'let
;; this is what define-macro turns into
(lambda (whole)
(apply (lambda (bindings . body)
(cons (cons 'lambda
(cons (map first bindings) body))
(map second bindings)))
(rest whole))))
Теперь вот что, но использование define-macro
для уменьшения боли:
(define-macro (let bindings . body)
;; Really primitive version
(cons (cons 'lambda (cons (map first bindings) body))
(map second bindings)))
И другая версия с использованиемlist*
, чтобы сделать вещи немного менее ужасными:
(define-macro (let bindings . body)
;; without backquote, but usung list* to make it a bit
;; less painful
(list* (list* 'lambda (map first bindings) body)
(map second bindings)))
И, наконец, версия, использующая обратную цитату ( aka квазицитат).
(define-macro (let bindings . body)
;; with backquote
`((lambda ,(map first bindings) ,@body)
,@(map second bindings)))
Вот версиямакроопределения для prog1
, которое нарушается из-за нарушения гигиены:
(define-macro (prog1 form . forms)
;; Broken
`(let ([r ,form])
,@forms
r))
И вот как вам нужно написать его, чтобы быть более гигиеничным (хотя он все еще негигиеничен по несколько экстремальным стандартам Схемы):
(define-macro (prog1 form . forms)
;; Working
(let ([rn (string->uninterned-symbol "r")])
`(let ([,rn ,form])
,@forms
,rn)))
Обратите внимание, что этот макрос превращается в другой макрос : он расширяется до let
: расширитель должен иметь дело с этим (и он делает).
Макроэкспандер
Макроэкспандер состоит из двух функций: expand-macros
- это то, что фактически выполняет расширение, и отправляется на expand-special
для специальных форм.
Вот expand-macros
:
(define (expand-macros form)
;; expanding a form
(if (cons? form)
;; only compound forms are even considered
(let ([op (first form)])
(cond [(macro? op)
;; it's a macro: call the macro function & recurse on the result
(expand-macros ((macro op) form))]
[(special-pattern? op)
;; it's special: use the special expander
(expand-special form)]
[else
;; just expand every element.
(map expand-macros form)]))
form))
Примечания по этому поводу:
- только соединениеформы могут быть макро-формами;
- это lisp-1, поэтому машины составных форм оцениваются полностью нормально и могут быть макро-формами:
((let (...) ...) ...)
отлично; - макросы расширеныдо тех пор, пока не останется ничего сделать.
Вот expand-special
: это намного сложнее, чем expand-macro
и, вероятно, глючит: он пытается сопоставить определение специальной формы с формой, которую ей дали.
(define (expand-special form)
;; expand a special thing based on a pattern.
(match-let* ([(cons op body) form]
[(cons pop pbody) (special-pattern op)])
(unless (eqv? op pop)
(error 'expand-special "~s is not ~s" pop op))
(let pattern-loop ([accum (list op)]
[tail body]
[ptail pbody]
[context 'expr])
(cond [(null? tail)
(unless (or (null? ptail)
(eqv? (first ptail) '...))
(error 'expand-special "~s is not enough forms for ~s"
body op))
(reverse accum)]
[(null? ptail)
(error 'expand-special "~s is too many forms for ~s"
body op)]
[else
(match-let* ([(cons btf btr) tail]
[(cons ptf ptr) ptail]
[ellipsis? (eqv? ptf '...)]
[ctx (if ellipsis? context ptf)]
[ptt (if ellipsis? ptail ptr)])
(pattern-loop (cons (if (eqv? ctx 'expr)
(expand-macros btf)
btf)
accum)
btr ptt ctx))]))))
Неприятный бит здесь - это обработка многоточия (...
), которое используется в сопоставителе для обозначения «здесь больше вещей»: я не могу вспомнить, может ли оно иметь дело с многоточием, которое не является последним в шаблон, но я подозреваю, что сильно нет. Обратите внимание, что хотя базовая система макросов также использует многоточие, они не связаны: это просто полагается на то, что ...
является допустимым именем символа.
Обратите внимание, что это возвращается в expand-macros
, где это необходимо, конечно.
С учетом этих определений мы теперь можем расширить некоторые макросы:
> (expand-macros '(let ((x y)) x))
'((lambda (x) x) y)
> (expand-macros '(prog1 a b))
'((lambda (r) b r) a)
Обратите внимание, что принтер Racket не печатает специально, а r
выше.
С помощью простой утилиты трассировки вы можете определить отслеживаемую версию макроэкспандера:
> (expand-macros '(let ([x 1]) (prog1 x (display "1"))))
[expand-macros (let ((x 1)) (prog1 x (display "1")))
[expand-macros ((lambda (x) (prog1 x (display "1"))) 1)
[expand-macros (lambda (x) (prog1 x (display "1")))
[expand-special (lambda (x) (prog1 x (display "1")))
[expand-macros (prog1 x (display "1"))
[expand-macros (let ((r x)) (display "1") r)
[expand-macros ((lambda (r) (display "1") r) x)
[expand-macros (lambda (r) (display "1") r)
[expand-special (lambda (r) (display "1") r)
[expand-macros (display "1")
[expand-macros display
-> display]
[expand-macros "1"
-> "1"]
-> (display "1")]
[expand-macros r
-> r]
-> (lambda (r) (display "1") r)]
-> (lambda (r) (display "1") r)]
[expand-macros x
-> x]
-> ((lambda (r) (display "1") r) x)]
-> ((lambda (r) (display "1") r) x)]
-> ((lambda (r) (display "1") r) x)]
-> (lambda (x) ((lambda (r) (display "1") r) x))]
-> (lambda (x) ((lambda (r) (display "1") r) x))]
[expand-macros 1
-> 1]
-> ((lambda (x) ((lambda (r) (display "1") r) x)) 1)]
-> ((lambda (x) ((lambda (r) (display "1") r) x)) 1)]
'((lambda (x) ((lambda (r) (display "1") r) x)) 1)
Доступна версия этого кода здесь .