Wrap a macro injecting bindings in another macro


I have in my toolbox the macro auto-hash-ref/:, defined here. Essentially, the idea is to automatically search for a variable value in a hash table. For example:

(define ht (hash 'a 1 'b 2))
(auto-hash-ref/: ht (+ :a (* 2 :b)))

The second line of this example expands to the following:

(let ([:a (hash-ref ht 'a)]
      [:b (hash-ref ht 'b)])
  (+ :a (* 2 :b)))

Now I would like to wrap auto-hash-ref/: in another macro to make function definition easier. I write the following code (everything is in Typed Racket):

(define-syntax-parse-rule (lambda/: (ht:id type:expr) body:expr)
  (lambda ([ht : type])
    (auto-hash-ref/: ht body)))

I try to use this macro in the following way:

(lambda/: (st (HashTable Symbol Integer)) (+ :a :b))

and I get the error (trimmed):

; :a: unbound identifier
;   in: :a

I compared the final expansion (obtained via Racket Mode's macro stepper) of auto-hash-ref/: and lambda/:. For (auto-hash-ref/: s (+ :a :b)) I obtain

(let-values (((:a) (#%app hash-ref (#%top . s) 'a))
             ((:b) (#%app hash-ref (#%top . s) 'b)))
  (#%app + :a :b))

For (lambda/: (st (HashTable Symbol Integer)) (+ :a :b)) I obtain

 (lambda (st)
   (let-values (((:a) (#%app hash-ref st 'a)) ((:b) (#%app hash-ref st 'b)))
     (#%app + (#%top . :a) (#%top . :b)))))

I remark that :a and :b in the second expansion are wrapped in #%top, which I think means that Racket looks for bindings for :a and :b in the wrong place.

How should I correctly write the macro lambda/:?

This is a classic program in the uncomposability of unhygenic macros. (This is discussed some in the syntax-parameters paper.) Basically, the interface of auto-hash-ref/: has as part of its interface the syntax of the whole form, which is where you get the syntactic context for all the variable bindings you insert. Then your lambda/: macro creates that syntax in its body, so that the bindings of :a and :b have the syntactic contexts of the expansion of lambda/: instead of its input.

The best solution is to (a) make it clear in the description of auto-hash-ref/: where it takes syntactic context from (b) choose something other than the whole form (such as the name of the hash table) and (c) ensure that the syntactic context of that comes from the input (which it does for that choice in your lambda/: macro). Basically you would change stx to #'id in the first argument to datum->syntax here.

Unfortunately, even if you follow those rules, you will often need to write unhygienic macros to abstract over auto-hash-ref/:. For example, if you want to write this macro:

(define-syntax-rule (auto-lambda body) (lambda (i) (auto-hash-ref/: i body)))

there is no way to write it as a hygienic macro and have it do what you want. Instead, even with the change I suggest, you would write:

(define-syntax (auto-lambda stx)
   (syntax-parse stx
     [(_ b:expr)
      #:with i (format-id #'b "i")
      (lambda (i) (auto-hash-ref/: i body))]))

Thank you very much @samth for your detailed explanations and suggestions! It turns out that the auto-lambda macro from your answer better corresponds to my use case.

I took some time to read and think over your message, and I finally start understanding why datum->syntax, format-id, etc. take a syntax context as an argument :smiley:

I guess am ready for going deeper on hygienic macros, so if anyone has references to suggest, please shoot.

The best introduction is Fear of Macros by @greghendershott.

1 Like

Thanks, reading that document one more time will definitely do me good.