Writing a `define-let` macro, with hygiene
I am trying to write a macro define-let
in racket that "saves" the header (let ((var value) ...) ...)
, namely only a part (var value) ...
, and allows it to be reused later.
The code below works as expected:
#lang racket ;; define-let allows saving the header part of a let, and re-use it later (define-syntax (define-let stx1) (syntax-case stx1 () [(_ name [var value] ...) #`(define-syntax (name stx2) (syntax-case stx2 () [(_ . body) #`(let ([#,(datum->syntax stx2 'var) value] ...) . body)]))])) ;; Save the header (let ([x "works]) ...) in the macro foo (define-let foo [x "works"]) ;; Use the header, should have the same semantics as: ;; (let ([x "BAD"]) ;; (let ([x "works]) ;; (displayln x)) (let ([x "BAD"]) (foo (displayln x))) ;; Displays "works".
The problem is that the macro violates hygiene: as shown in the example below, the variable y
declared in define-let
, which is created by the macro, must be a new, non-terminated symbol due to hygiene, but it manages to leak out of the macro and is mistakenly accessed in (displayln y)
.
;; In the following macro, hygiene should make y unavailable (define-syntax (hygiene-test stx) (syntax-case stx () [(_ name val) #'(define-let name [y val])])) ;; Therefore, the y in the above macro shouldn't bind the y in (displayln y). (hygiene-test bar "wrong") (let ((y "okay")) (bar (displayln y))) ;; But it displays "wrong".
How do I write a macro define-let
so that it behaves like in the first example, but also preserves hygiene when the identifier is generated by the macro, giving "okay"
in the second example?
Following the "Syntax-Parameter" syntax parameter from Chris, here is one solution:
#lang racket
(require racket/stxparam
(for-syntax syntax/strip-context))
(define-syntax (define-let stx1)
(syntax-case stx1 ()
[(_ name [var expr] ...)
(with-syntax ([(value ...) (generate-temporaries #'(expr ...))])
#`(begin
(define-syntax-parameter var (syntax-rules ()))
...
(define value expr)
...
(define-syntax (name stx2)
(syntax-case stx2 ()
[(_ . body)
(with-syntax ([body (replace-context #'stx1 #'body)])
#'(syntax-parameterize ([var (syntax-id-rules () [_ value])] ...)
. body))]))))]))
(define-let foo [x "works"])
(let ([x "BAD"])
(foo (displayln x))) ; => works
(let ([x "BAD"])
(foo
(let ([x "still works"])
(displayln x)))) ; => still works
UPDATE
This solution conveys additional test in the comments. The new solution transfers the body context to the variables to be bound.
#lang racket
(require (for-syntax syntax/strip-context))
(define-syntax (define-let stx1)
(syntax-case stx1 ()
[(_ name [var expr] ...)
#`(begin
(define-syntax (name stx2)
(syntax-case stx2 ()
[(_ . body)
(with-syntax ([(var ...) (map (Ξ» (v) (replace-context #'body v))
(syntax->list #'(var ...)))])
#'(let ([var expr] ...)
. body))])))]))
(define-let foo [x "works"])
(let ([x "BAD"])
(foo (displayln x))) ; => works
(let ([x "BAD"])
(foo
(let ([x "still works"])
(displayln x)))) ; => still works
(let ([z "cool"])
(foo (displayln z))) ; => cool