Захват макросов в схеме
Какой самый простой способ определить макрос захвата, используя 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 является лучшим решением.