3

I'm trying to write a define-let macro in racket, which "saves" the header of a (let ((var value) ...) ...) , namely just the (var value) ... part, and allows re-using it later on.

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 breaks hygiene: as shown in the example below, the variable y, declared in a define-let which is produced by a macro, should be a new, uninterned symbol, due to hygiene, but it manages to leak out of the macro, and it is erroneously accessible 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 can I write the define-let macro so that it behaves like in the first example, but also preserves hygiene when the identifier is generated by a macro, giving "okay" in the second example?

Suzanne Soy
  • 3,027
  • 6
  • 38
  • 56
  • 3
    If I understood your use cases correctly, you need to use parameters, which establishes a "scope" for the overridden `x`: so in your first case, you'd establish a parameter that is in scope for your top-level `define-let` invocation, but would be out of scope for your macro-that-calls-`define-let`. Since you're binding data rather than syntax for your `x`, you'd use normal parameters, but Racket also provides syntax parameters for cases that need it. – C. K. Young Jun 08 '15 at 15:08
  • @ChrisJester-Young I had tried using define-syntax-parameter, but then the binding it introduces is available everywhere, not just inside the (bar ...) part. How could I avoid that? – Suzanne Soy Jun 08 '15 at 15:13
  • It's technically available in top-level (assuming that's where your syntax parameter is defined), but if you make its default binding call `raise-syntax-error` or the like *during the macro-expansion phase*, it may as well be the same as not existing. – C. K. Young Jun 08 '15 at 16:28

1 Answers1

3

Following the cue "syntax-parameter" from Chris, here is an 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 passes the additional test in the comments. The new solution transfers the context of the body 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
soegaard
  • 30,661
  • 4
  • 57
  • 106
  • The use of `replace-context` is very interesting, I didn't know about it. However it strips away all the bindings available where `foo` is used: `(define-let foo [x "unused"]) (let ([z "cool"]) (foo (displayln z)))` causes an `unbound identifier` error for `z`, and `define-let` does not really behave as if the real let was put there. I'll look more in-depth at the syntax-helpers part of the documentation, maybe I'll find a way to extend the syntax context of `#'body` with the bindings that would have been introduced by the `let` if it had been written around it. – Suzanne Soy Jun 09 '15 at 14:35
  • @GeorgesDupéron Hi have added a new solution that passes the additional test. – soegaard Jun 09 '15 at 21:13