user2162871
user2162871

Reputation: 409

evaluating forms during macro expansion in Racket

This Common Lisp macro and test function

(defmacro test (body)
  `(let ,(mapcar #'(lambda (s)
             `(,s ,(char-code (char-downcase (char (symbol-name s) 0)))))
             '(a b))
     ,body))

(test (+ a b))

expands into

(let ((a 97) (b 98))
  (+ a b))

and gives 195 when evaluated

Trying to do that in Racket

(define-syntax (test stx)
  (syntax-case stx ()
    [(_ body)
     #`(let #,(map (lambda (x)
                     (list x
                           (char->integer (car (string->list (symbol->string x))))))
                   '(a b))
         body)]))

(test (+ a b))

When I run the macroexpander, the macro form expands to:

(let ((a 97) (b 98)) (+ a b))))

which is what I thought I wanted.

But it fails with:

a: unbound identifier in context..

Disabling macro hiding gives a form that ends with:

(#%app:35
 call-with-values:35
 (lambda:35 ()
  (let-values:36 (((a:37) (quote 97)) ((b:37) (quote 98)))
   (#%app:38 + (#%top . a) b)))
  (print-values:35)))

I don't understand why my nice expansion (let ((a 97) (b 98)) (+ a b)) doesn't work, and I'm puzzled by (#%top .a)... I wonder if it's trying to find a function called "a"? When I copy the expanded form into the REPL, it works...

I'm grateful for any help!

Upvotes: 2

Views: 199

Answers (2)

user5920214
user5920214

Reputation:

As a counterpart Sorawee Porncharoenwase's answer (which is the right answer) I think it's worth while thinking a bit about why your test macro is problematic in CL and why macros which do similar things are outright buggy.

Given your test macro, imagine some user looking at this code:

(let ((a 1) (b 2))
  (test (+ a b)))

Well, I don't know about you, but what I would expect to happen is that the a and b inside test are the a and b I've just bound. But that's not the case at all, of course.

Well, perhaps the documentation for test describes in great detail that it binds two variables, and that this is what I should expect. And, of course, there are macros which do just that, and where it's fine:

(defmacro awhen (test &body forms)
  `(let ((it ,test))
     (when ,it ,@forms)))

And now:

(awhen (find-exploder thing)
  (explode it))

And this is all fine, because the documentation for awhen will say that it binds it to the result of the test in its body.

But now consider this or macro stolen from the other answer:

(defmacro vel (a b)
  `(let ((a-val ,a))
     (if a-val a-val ,b)))

This is a disaster. It 'works', except it doesn't work at all:

> (let ((a-val 3))
    (vel nil a-val))
nil

Now that's not just surprising in the way your test macro is: it's wrong.

Instead, you have to write vel like this in CL:

(defmacro vel (a b)
  (let ((a-val-name (make-symbol "A-VAL")))
    `(let ((,a-val-name ,a))
       (if ,a-val-name ,a-val-name ,b))))

(You can of course use gensym instead of make-symbol, and most people do I think.)

And now

> (let ((a-val 3))
  (vel nil a-val))
3

as you would expect.

This is all because the CL macro system is unhygenic – it relies on you to ensure that things like names do not clash. In CL you have to go slightly out of your way to write macros which are correct in many cases. The Racket macro system, on the other hand, is hygenic: it will by default ensure that names (and other things) don't clash. In Racket (and Scheme) you have to go out of your way to write macros which are either incorrect or do something slightly unexpected like introducing bindings visible from code which makes use of the macros.

Note that I'm not expressing a preference for either approach to macros: I've spent most of my life writing CL, and I'm very happy with its macro system. More recently I've written more Racket and I'm happy with its macro system as well, although I find it harder to understand.

Finally here is a variant of your macro which is less surprising in use (almost all of the noise in this code is sanity checking which syntax-parse supports in the form of the two #:fail-when clauses):

(define-syntax (with-char-codes stx)
  (syntax-parse stx
    [(_ (v:id ...) form ...)
     #:fail-when (check-duplicate-identifier (syntax->list #'(v ...)))
     "duplicate name"
     #:fail-when (for/or ([name (syntax->list #'(v ...))])
                   (and (> (string-length (symbol->string
                                           (syntax->datum name)))
                           1)
                        name))
     "name too long"
     #'(let ([v (char->integer (string-ref (symbol->string 'v) 0))] ...)
         form ...)]))

And now

> (with-char-codes (a b)
    (+ a b))
195

Upvotes: 3

Sorawee Porncharoenwase
Sorawee Porncharoenwase

Reputation: 6502

Racket has hygienic macro. Consider:

(define-syntax-rule (or a b)
  (let ([a-val a]) 
    (if a-val a-val b)))

Then:

(let ([a-val 1])
  (or #f a-val))

will roughly expand to:

(let ([a-val 1])
  (let ([a-val2 #f]) 
    (if a-val2 a-val2 a-val)))

which evaluates to 1. If macro is not hygienic, then it would result in #f, which is considered incorrect.

Notice that a-val is renamed to a-val2 automatically to avoid the collision. That's what happens to your case too.

One way to fix the problem in your case is to give a correct context to the generated identifiers so that the macroexpander understands that they should refer to the same variable.

(define-syntax (test stx)
  (syntax-case stx ()
    [(_ body)
     #`(let #,(map (lambda (x)
                     (list (datum->syntax stx x) ; <-- change here
                           (char->integer (car (string->list (symbol->string x))))))
                   '(a b))
         body)]))

(test (+ a b))

Upvotes: 3

Related Questions