Capturing macros on a chart

What is the easiest way to define macro capture using define-syntax or define-syntax-rule in Racket?

As a concrete example, here is a trivial aif in the 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 test result in the if-true and if-false clauses. Naive transliteration (minus an optional alternative) -

 (define-syntax-rule (aif test if-true if-false) (let ((it test)) (if it if-true if-false))) 

which is evaluated without complaint, but an error if you try to use it in sentences:

 > (aif "Something" (displayln it) (displayln "Nope"))) reference to undefined identifier: it 

anaphora egg implements aif like

 (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 does not have an ir-macro-transformer defined or documented.

+6
source share
3 answers

Racket macros are designed to prevent capture by default. When you use define-syntax-rule , it will respect the lexical domain.

If you want to “break hygiene” on purpose, traditionally in the Scheme, you should use the syntax-case and (carefully) use datum->syntax .

But in Racket, the easiest and safest way to make “anaphoric” macros is the syntax parameter and the simple define-syntax-rule .

For instance:

 (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 the Eli Barzilay Dirty Looking Hygiene blog post and Saving it using syntax parameters (PDF) .

+10
source

See the Greg Hendershott Macro Tutorial. This section uses anaphoric if, as an example:

http://www.greghendershott.com/fear-of-macros/Syntax_parameters.html

+4
source

Although the above answer is a common way to implement aif in the Racket community, it has serious flaws. In particular, you can obscure it by specifying a local variable named it .

 (let ((it 'gets-in-the-way)) (aif 'what-i-intended (display it))) 

In the above example, gets-in-the-way will be displayed instead of what-i-intended , although aif defines its own variable called it . The outer form of let makes the definition of aif inner let invisible. This is what the Scheme community wants. In fact, they want you to write code that behaves so badly so that they vote to delete my original answer when I did not agree that their method was better.

There is no error-free way to write captured macros to Scheme. The closest thing you can do is go to the syntax tree that may contain the variables that you want to capture and explicitly ban the scope information contained in them, replacing it with new scope information that forces them to reference your local versions of these 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)))))) 

This gives you the capture-vars , which allows you to explicitly specify variables from the body you want to capture. aif can be written as follows:

 (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 aif I defined work as a regular Scheme if in that an else clause is optional.

Unlike the answer above, it really fixed. This is not just a global variable:

  (let ((it 'gets-in-the-way)) (aif 'what-i-intended (display it))) 

Inadequacy of using only one datum->syntax call

Some people think that all you need to do to create a capture macro is to use datum->syntax in one of the top forms passed to your macro, for example:

 (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 for recording macros. It will work in most cases, but break in some cases, especially if you include a capture macro written in this way in another macro. The above macro will only capture it if expr comes from the same area as true-expr . If they come from different areas (this can only happen by wrapping the user expr in the form generated by your macro), then it in true-expr will not be captured, and you will be left with the question: "WTF will it be captured?"

You may be tempted to quickly fix this by using (datum->syntax #'true-expr 'it) instead of (datum->syntax #'expr 'it) . Actually, this makes the problem worse, since now you cannot use aif to determine 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 determined using the capture-vars , then the above will work as expected. But if it is defined using datum->syntax on true-expr , adding begin to the bodies will cause it be visible in the scope of the acond macro instead of the code that is called by acond .

Inability to really record capture macro in Racket

This example has been brought to my attention and demonstrates why you simply cannot write a real capture macro in a 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 capture-vars it in alias-it , because it will not be in the AST until aif finishes the extension.

This problem cannot be fixed because the definition of the alias-it macro is most likely not visible from the scope of the aif macro aif . Therefore, when you try to expand it in aif , perhaps using expand , alias-it will be considered as a function. Testing shows that lexical information tied to alias-it does not cause it to be recognized as a macro to determine a macro taken out of scope from alias-it .

Some argue that this suggests why solving a syntax parameter is an excellent solution, but perhaps it really shows why writing code in Common Lisp is an excellent solution.

+1
source

Source: https://habr.com/ru/post/958328/


All Articles