Захват макросов в схеме

Какой самый простой способ определить макрос захвата, используя define-syntax или же define-syntax-rule в ракетку?

В качестве конкретного примера, вот тривиальный aif в макросистеме в стиле CL.

(defmacro aif (test if-true &optional if-false)
    `(let ((it ,test))
        (if it ,if-true ,if-false)))

Идея в том, что it будет связан с результатом test в if-true а также if-false статьи. Наивная транслитерация (минус необязательная альтернатива)

(define-syntax-rule (aif test if-true if-false)
    (let ((it test))
       (if it if-true if-false)))

который оценивает без жалоб, но ошибки, если вы пытаетесь использовать it в пунктах:

> (aif "Something" (displayln it) (displayln "Nope")))
reference to undefined identifier: it

anaphora яйцо орудий aif как

(define-syntax aif
  (ir-macro-transformer
   (lambda (form inject compare?)
     (let ((it (inject 'it)))
       (let ((test (cadr form))
         (consequent (caddr form))
         (alternative (cdddr form)))
     (if (null? alternative)
         `(let ((,it ,test))
        (if ,it ,consequent))
         `(let ((,it ,test))
        (if ,it ,consequent ,(car alternative)))))))))

но ракетка вроде бы не ir-macro-transformer определено или задокументировано.

3 ответа

Решение

Макросы ракетки по умолчанию предназначены для предотвращения захвата. Когда вы используете define-syntax-rule это будет уважать лексическую сферу.

Если вы хотите "нарушать гигиену" намеренно, традиционно в схеме вы должны использовать syntax-case и (осторожно) использовать datum->syntax,

Но в Racket самый простой и безопасный способ сделать "анафорические" макросы - это с помощью параметра синтаксиса и простого define-syntax-rule,

Например:

(require racket/stxparam)

(define-syntax-parameter it
  (lambda (stx)
    (raise-syntax-error (syntax-e stx) "can only be used inside aif")))

(define-syntax-rule (aif condition true-expr false-expr)
  (let ([tmp condition])
    (if tmp
        (syntax-parameterize ([it (make-rename-transformer #'tmp)])
          true-expr)
        false-expr)))

Я написал о параметрах синтаксиса здесь, а также вы должны прочитать сообщение в блоге Эли Барзилая " Грязная гигиена" и " Содержать его в чистоте с помощью параметров синтаксиса" (PDF).

Посмотрите учебник по макросам Грега Хендершотта. Этот раздел использует анафорический, если в качестве примера:

http://www.greghendershott.com/fear-of-macros/Syntax_parameters.html

Хотя ответ выше - это общепринятый способ внедрения aif в сообщество Racket, у него есть серьезные недостатки. В частности, вы можете тень it путем определения локальной переменной с именем it,

(let ((it 'gets-in-the-way))
     (aif 'what-i-intended
          (display it)))

Выше будет отображаться gets-in-the-way вместо what-i-intended, даже если aif определяет свою собственную переменную с именем it, Внешний let форма оказывает aifвнутренний let определение невидимо. Это то, чего хочет сообщество Scheme. На самом деле, они хотят, чтобы вы написали код, который ведет себя так плохо, что они проголосовали за удаление моего исходного ответа, когда я не признаю, что их путь был лучше.

Не существует безошибочного способа записи макросов захвата в Scheme. Самое близкое, что вы можете получить - это пройтись по синтаксическому дереву, которое может содержать переменные, которые вы хотите захватить, и явно убрать содержащуюся в них информацию о области видимости, заменив ее новой информацией о области видимости, которая заставляет их ссылаться на ваши локальные версии этих переменных. Я написал три функции для синтаксиса и макрос, чтобы помочь с этим:

(begin-for-syntax
 (define (contains? atom stx-list)
   (syntax-case stx-list ()
     (() #f)
     ((var . rest-vars)
      (if (eq? (syntax->datum #'var)
               (syntax->datum atom))
          #t
          (contains? atom #'rest-vars)))))

 (define (strip stx vars hd)
   (if (contains? hd vars)
       (datum->syntax stx
                      (syntax->datum hd))
       hd))

 (define (capture stx vars body)
   (syntax-case body ()
     (() #'())
     (((subform . tl) . rest)
      #`(#,(capture stx vars #'(subform . tl)) . #,(capture stx vars #'rest)))
     ((hd . tl)
      #`(#,(strip stx vars #'hd) . #,(capture stx vars #'tl)))
     (tl (strip stx vars #'tl)))))

(define-syntax capture-vars
  (λ (stx)
     (syntax-case stx ()
         ((_ (vars ...) . body)
          #`(begin . #,(capture #'(vars ...) #'(vars ...) #'body))))))

Это дает вам capture-vars макрос, который позволяет вам явно назвать переменные из тела, которое вы хотите захватить. aif тогда можно написать так:

(define-syntax aif
  (syntax-rules ()
       ((_ something true false)
        (capture-vars (it)
           (let ((it something))
            (if it true false))))
       ((_ something true)
        (aif something true (void)))))

Обратите внимание, что aif Я определил работы как обычные схемы if в том, что условие else является необязательным.

В отличие от ответа выше, it действительно захвачен. Это не просто глобальная переменная:

 (let ((it 'gets-in-the-way))
     (aif 'what-i-intended
          (display it)))

Неадекватность простого использования одного звонка datum->syntax

Некоторые люди думают, что все, что вам нужно сделать, чтобы создать макрос захвата, это использовать datum->syntax в одной из верхних форм, передаваемых вашему макросу, вот так:

(define-syntax aif
  (λ (stx)
     (syntax-case stx ()
       ((_ expr true-expr false-expr)
        (with-syntax
            ((it (datum->syntax #'expr 'it)))
            #'(let ((it expr))
                (if it true-expr false-expr))))
       ((_ expr true-expr)
        #'(aif expr true-expr (void))))))

Просто используя datum->syntax это только 90% решение для написания захвата макросов. В большинстве случаев это будет работать, но в некоторых случаях прерываться, особенно если вы включаете записывающий макрос, написанный таким образом, в другой макрос. Приведенный выше макрос будет только захватывать it если expr происходит из той же области, что и true-expr, Если они приходят из разных областей (это может произойти просто путем обертывания пользователя expr в форме, сгенерированной вашим макросом), то it в true-expr не будут захвачены, и вы будете задавать себе вопрос "WTF не будет захватывать?"

Вы можете испытать желание быстро исправить это, используя (datum->syntax #'true-expr 'it) вместо (datum->syntax #'expr 'it), На самом деле это усугубляет проблему, так как теперь вы не сможете использовать aif определить acond:

(define-syntax acond
    (syntax-rules (else)
        ((_) (void))
        ((_ (condition . body) (else . else-body))
         (aif condition (begin . body) (begin . else-body)))
        ((_ (condition . body) . rest)
         (aif condition (begin . body) (acond . rest)))))

Если aif определяется с помощью capture-vars макрос, выше будет работать, как ожидалось. Но если это определяется с помощью datum->syntax на true-exprдобавление begin к телам приведет к it быть видимым в объеме acondопределение макроса вместо кода, который вызвал acond,

Невозможность действительно написать захватывающий макрос в Racket

Этот пример был представлен моему вниманию и демонстрирует, почему вы просто не можете написать настоящий макрос захвата в Scheme:

(define-syntax alias-it
  (syntax-rules ()
     ((_ new-it . body)
      (let ((it new-it)) . body))))

(aif (+ 1 2) (alias-it foo ...))

capture-vars не может захватить it в alias-itмакроразложение, потому что оно не будет на AST, пока после aif закончила расширяться.

Это вообще невозможно исправить, так как определение макроса alias-it скорее всего, не видно из области aifмакроопределение. Поэтому, когда вы пытаетесь расширить его внутри aifвозможно, используя expand, alias-it будет рассматриваться как функция. Тестирование показывает, что лексическая информация прилагается к alias-it не заставляет его распознаваться как макрос для определения макроса, написанного вне области видимости alias-it,

Некоторые утверждают, что это показывает, почему решение с синтаксическим параметром является лучшим решением, но, возможно, оно действительно показывает, почему написание кода в Common Lisp является лучшим решением.

Другие вопросы по тегам