Search code examples
macrosschemechicken-scheme

What is the difference between these macros?


I have some questions about how macros work in Scheme (specifically in Chicken Scheme), let's consider this example:

(define (when-a condition . body)
  (eval `(if ,condition
     (begin ,@body)
     '())))

(define-syntax when-b
  (er-macro-transformer
    (lambda (exp rename compare)
      (let ((condition (cadr exp))
            (body (cddr exp)))
        `(if ,condition (begin ,@body) '())))))

(define-syntax when-c
  (ir-macro-transformer
    (lambda (exp inject compare)
      (let ((condition (cadr exp))
            (body (cddr exp)))
        `(if ,condition (begin ,@body) '())))))

(define-syntax when-d
  (syntax-rules ()
    ((when condition body ...)
     (if condition (begin body ...) '()))))
  1. Can I consider when-a a macro? I feel that I can't consider it a macro in a strict way since I'm not using define-syntax but I'm not able to say any pratical reason to not prefer this implementation.

  2. Are my macros hygienic?

  3. Is there any difference between when-b and when-c? Since I'm not using rename nor inject I think there isn't.


Solution

  • Can I consider when-a a macro? I feel that I can't consider it a macro in a strict way since I'm not using define-syntax but I'm not able to say any pratical reason to not prefer this implementation.

    This works like a macro, but it's not exactly the same as a true macro, for the following reasons:

    • The main difference between a true macro and your eval-based "macro" is that your approach will evaluate all its arguments before calling it. This is a very important difference. For example: (if #f (error "oops") '()) will evaluate to '() but (when-a #f (error "oops")) will raise an error.
    • It's not hygienic. Beforehand, one might have done something like (eval '(define if "not a procedure")), for example, and that would mean this eval would fail; the if in the body expression of the "expansion" doesn't refer to the if at the definition site.
    • It does not get expanded at compile time. This is another major reason to use a macro; the compiler will expand it, and at runtime no computation will be performed to perform the expansion. The macro itself will have completely evaporated. Only the expansion remains.

    Are my macros hygienic?

    Only when-c and when-d are, because of the guarantees made by ir-macro-transformer and syntax-rules. In when-b you'd have to rename if and begin to make them refer to the versions of if and begin at the macro definition site.

    Example:

    (let ((if #f))
      (when-b #t (print "Yeah, ok")))
    
    == expands to ==>
    
    (let ((if1 #f))
      (if1 #t (begin1 (print "Yeah, ok"))))
    

    This will fail, because both versions of if (here annotated with an extra 1 suffix) refer to the same thing, so we'll end up calling #f in operator position.

    In contrast,

    (let ((if #f))
      (when-c #t (print "Yeah, ok")))
    
    == expands to ==>
    
    (let ((if1 #f))
      (if2 #t (begin1 (print "Yeah, ok"))))
    

    Which will work as intended. If you want to rewrite when-b to be hygienic, do it like this:

    (define-syntax when-b
      (er-macro-transformer
        (lambda (exp rename compare)
          (let ((condition (cadr exp))
                (body (cddr exp))
                (%if (rename 'if))
                (%begin (rename 'begin)))
            `(,%if ,condition (,%begin ,@body) '())))))
    

    Note the extra %-prefixed identifiers which refer to the original value of if and begin as they were at the place of definition of the macro.

    Is there any difference between when-b and when-c? Since I'm not using rename nor inject I think there isn't.

    There is. Implicit renaming macros are called that because they implicitly rename all the identifiers that come in from the usage site, and also every new identifier you introduce in the body. If you inject any identifiers, that undoes this implicit renaming, which makes them unhygienically available for capture by the calling code.

    On the other hand, explicit renaming macros are called that because you must explicitly rename any identifiers to prevent them being captured by the calling code.