Написание макроса `define-let` с гигиеной

Я пытаюсь написать define-let макрос в ракетке, который "сохраняет" заголовок (let ((var value) ...) ...) а именно только (var value) ... часть, и позволяет повторно использовать это позже.

Код ниже работает как ожидалось:

#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".

Проблема в том, что макрос нарушает гигиену: как показано в примере ниже, переменная yзаявлен в define-let который создается макросом, должен быть новым, непостоянным символом из-за гигиены, но ему удается вытечь из макроса, и он ошибочно доступен в (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".

Как я могу написать define-let макрос, так что он ведет себя как в первом примере, но также сохраняет гигиену, когда идентификатор генерируется макросом, давая "okay" во втором примере?

1 ответ

Решение

После реплики "синтаксис-параметр" от Криса, вот одно решение:

#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

ОБНОВИТЬ

Это решение проходит дополнительный тест в комментариях. Новое решение передает контекст тела переменным для привязки.

#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
Другие вопросы по тегам