问题
What's the simplest way to define a capturing macro using define-syntax
or define-syntax-rule
in Racket?
As a concrete example, here's the trivial aif
in a CL-style macro system.
(defmacro aif (test if-true &optional if-false)
`(let ((it ,test))
(if it ,if-true ,if-false)))
The idea is that it
will be bound to the result of test
in the if-true
and if-false
clauses. The naive transliteration (minus optional alternative) is
(define-syntax-rule (aif test if-true if-false)
(let ((it test))
(if it if-true if-false)))
which evaluates without complaint, but errors if you try to use it
in the clauses:
> (aif "Something" (displayln it) (displayln "Nope")))
reference to undefined identifier: it
The anaphora egg implements aif
as
(define-syntax aif
(ir-macro-transformer
(lambda (form inject compare?)
(let ((it (inject 'it)))
(let ((test (cadr form))
(consequent (caddr form))
(alternative (cdddr form)))
(if (null? alternative)
`(let ((,it ,test))
(if ,it ,consequent))
`(let ((,it ,test))
(if ,it ,consequent ,(car alternative)))))))))
but Racket doesn't seem to have ir-macro-transformer
defined or documented.
回答1:
Racket macros are designed to avoid capture by default. When you use define-syntax-rule
it will respect lexical scope.
When you want to "break hygiene" intentionally, traditionally in Scheme you have to use syntax-case
and (carefully) use datum->syntax
.
But in Racket the easiest and safest way to do "anaphoric" macros is with a syntax parameter and the simple define-syntax-rule
.
For example:
(require racket/stxparam)
(define-syntax-parameter it
(lambda (stx)
(raise-syntax-error (syntax-e stx) "can only be used inside aif")))
(define-syntax-rule (aif condition true-expr false-expr)
(let ([tmp condition])
(if tmp
(syntax-parameterize ([it (make-rename-transformer #'tmp)])
true-expr)
false-expr)))
I wrote about syntax parameters here and also you should read Eli Barzilay's Dirty Looking Hygiene blog post and Keeping it Clean with Syntax Parameters paper (PDF).
回答2:
See Greg Hendershott's macro tutorial. This section uses anaphoric if as example:
http://www.greghendershott.com/fear-of-macros/Syntax_parameters.html
回答3:
Although the answer above is the accepted way to implement aif in the Racket community, it has severe flaws. Specifically, you can shadow it
by defining a local variable named it
.
(let ((it 'gets-in-the-way))
(aif 'what-i-intended
(display it)))
The above would display gets-in-the-way
instead of what-i-intended
, even though aif
is defining its own variable named it
. The outer let
form renders aif
's inner let
definition invisible. This is what the Scheme community wants to happen. In fact, they want you to write code that behaves like this so badly, that they voted to have my original answer deleted when I wouldn't concede that their way was better.
There is no bug-free way to write capturing macros in Scheme. The closest you can come is to walk down the syntax tree that may contain variables you want to capture and explicitly strip the scoping information that they contain, replacing it with new scoping information that forces them to refer to your local versions of those variables. I wrote three "for-syntax" functions and a macro to help with this:
(begin-for-syntax
(define (contains? atom stx-list)
(syntax-case stx-list ()
(() #f)
((var . rest-vars)
(if (eq? (syntax->datum #'var)
(syntax->datum atom))
#t
(contains? atom #'rest-vars)))))
(define (strip stx vars hd)
(if (contains? hd vars)
(datum->syntax stx
(syntax->datum hd))
hd))
(define (capture stx vars body)
(syntax-case body ()
(() #'())
(((subform . tl) . rest)
#`(#,(capture stx vars #'(subform . tl)) . #,(capture stx vars #'rest)))
((hd . tl)
#`(#,(strip stx vars #'hd) . #,(capture stx vars #'tl)))
(tl (strip stx vars #'tl)))))
(define-syntax capture-vars
(λ (stx)
(syntax-case stx ()
((_ (vars ...) . body)
#`(begin . #,(capture #'(vars ...) #'(vars ...) #'body))))))
That gives you the capture-vars
macro, which allows you to explicitly name the variables from the body you'd like to capture. aif
can then be written like this:
(define-syntax aif
(syntax-rules ()
((_ something true false)
(capture-vars (it)
(let ((it something))
(if it true false))))
((_ something true)
(aif something true (void)))))
Note that the aif
I have defined works like regular Scheme's if
in that the else-clause is optional.
Unlike the answer above, it
is truly captured. It's not merely a global variable:
(let ((it 'gets-in-the-way))
(aif 'what-i-intended
(display it)))
The inadequacy of just using a single call to datum->syntax
Some people think that all you have to do to create a capturing macro is use datum->syntax
on one of the top forms passed to your macro, like this:
(define-syntax aif
(λ (stx)
(syntax-case stx ()
((_ expr true-expr false-expr)
(with-syntax
((it (datum->syntax #'expr 'it)))
#'(let ((it expr))
(if it true-expr false-expr))))
((_ expr true-expr)
#'(aif expr true-expr (void))))))
Just using datum->syntax
is only a 90% solution to writing capturing macros. It will work in most cases, but break in some cases, specifically if you incorporate a capturing macro written this way in another macro. The above macro will only capture it
if the expr
comes from the same scope as the true-expr
. If they come from different scopes (this can happen merely by wrapping the user's expr
in a form generated by your macro), then the it
in true-expr
will not be captured and you'll be left asking yourself "WTF won't it capture?"
You may be tempted to quick-fix this by using (datum->syntax #'true-expr 'it)
instead of (datum->syntax #'expr 'it)
. In fact this makes the problem worse, since now you won't be able to use aif
to define acond
:
(define-syntax acond
(syntax-rules (else)
((_) (void))
((_ (condition . body) (else . else-body))
(aif condition (begin . body) (begin . else-body)))
((_ (condition . body) . rest)
(aif condition (begin . body) (acond . rest)))))
If aif
is defined using the capture-vars
macro, the above will work as expected. But if it's defined by using datum->syntax
on the true-expr
, the the addition of begin
to the bodies will result in it
being visible in the scope of acond
's macro definition instead of the code that invoked acond
.
The impossibility of really writing a capturing macro in Racket
This example was brought to my attention, and demonstrates why you just can't write a real capturing macro in Scheme:
(define-syntax alias-it
(syntax-rules ()
((_ new-it . body)
(let ((it new-it)) . body))))
(aif (+ 1 2) (alias-it foo ...))
capture-vars
cannot capture the it
in alias-it
's macroexpansion, because it won't be on the AST until after aif
is finished expanding.
It is not possible at all to fix this problem, because the macro definition of alias-it
is most probably not visible from the scope of aif
's macro definition. So when you attempt to expand it within aif
, perhaps by using expand
, alias-it
will be treated as a function. Testing shows that the lexical information attached to alias-it
does not cause it to be recognized as a macro for a macro definition written out of scope from alias-it
.
Some would argue that this shows why the syntax-parameter solution is the superior solution, but perhaps what it really shows is why writing your code in Common Lisp is the superior solution.
来源:https://stackoverflow.com/questions/20054263/capturing-macros-in-scheme