оценка форм при расширении макроса в Racket - PullRequest
2 голосов
/ 15 марта 2020

Этот макрос и тестовая функция Common Lisp

(defmacro test (body)
  `(let ,(mapcar #'(lambda (s)
             `(,s ,(char-code (char-downcase (char (symbol-name s) 0)))))
             '(a b))
     ,body))

(test (+ a b))

расширяется до

(let ((a 97) (b 98))
  (+ a b))

и дает 195 при оценке

Попытка сделать это в Racket

(define-syntax (test stx)
  (syntax-case stx ()
    [(_ body)
     #`(let #,(map (lambda (x)
                     (list x
                           (char->integer (car (string->list (symbol->string x))))))
                   '(a b))
         body)]))

(test (+ a b))

Когда я запускаю макроэкспандер, форма макроса расширяется до:

(let ((a 97) (b 98)) (+ a b))))

, что, как я думал, я хотел.

Но это не с:

a: unbound identifier in context..

Отключение скрытия макросов дает форму, которая заканчивается на:

(#%app:35
 call-with-values:35
 (lambda:35 ()
  (let-values:36 (((a:37) (quote 97)) ((b:37) (quote 98)))
   (#%app:38 + (#%top . a) b)))
  (print-values:35)))

Я не понимаю, почему мое хорошее расширение (let ((a 97) (b 98)) (+ a b)) не работает, и я озадачен (#% top .a) ... Интересно, пытается ли она найти функцию с именем "a"? Когда я копирую расширенную форму в REPL, она работает ...

Я благодарен за любую помощь!

Ответы [ 2 ]

3 голосов
/ 16 марта 2020

В качестве аналога Ответ Sorawee Porncharoenwase (который является правильным ответом) Я думаю, стоит задуматься над тем, почему ваш макрос test проблематичен c в CL и почему макросы, которые делают подобные все просто глючит.

Учитывая ваш макрос test, представьте, что какой-то пользователь смотрит на этот код:

(let ((a 1) (b 2))
  (test (+ a b)))

Ну, я не знаю о вас, но чего бы я ожидал случиться так, что a и b внутри test - это a и b, которые я только что связал. Но это совсем не так, конечно.

Что ж, возможно, документация для test очень подробно описывает, что она связывает две переменные, и это то, чего я должен ожидать. И, конечно, есть макросы, которые делают именно это, и где это нормально:

(defmacro awhen (test &body forms)
  `(let ((it ,test))
     (when ,it ,@forms)))

А теперь:

(awhen (find-exploder thing)
  (explode it))

И это все хорошо, потому что документация для awhen скажет, что оно связывает it с результатом теста в своем теле.

Но теперь рассмотрим этот or макрос, украденный из другого ответа:

(defmacro vel (a b)
  `(let ((a-val ,a))
     (if a-val a-val ,b)))

Это это катастрофа. Он «работает», за исключением того, что он вообще не работает:

> (let ((a-val 3))
    (vel nil a-val))
nil

Теперь это не просто удивительно в том, как ваш макрос test: он неправильный.

Вместо этого вы нужно написать vel как это в CL:

(defmacro vel (a b)
  (let ((a-val-name (make-symbol "A-VAL")))
    `(let ((,a-val-name ,a))
       (if ,a-val-name ,a-val-name ,b))))

(Вы, конечно, можете использовать gensym вместо make-symbol, и я думаю, что большинство людей.)

И теперь

> (let ((a-val 3))
  (vel nil a-val))
3

, как и следовало ожидать.

Это все потому, что система макросов CL unhygeni c - она ​​гарантирует, что такие вещи, как имена не кла sh. В CL вы должны go немного уйти от написания макросов, которые во многих случаях верны. Макросистема Racket, с другой стороны, имеет вид hygeni c: по умолчанию она гарантирует, что имена (и другие вещи) не будут sh. В Racket (и Scheme) вы должны go из своего способа писать макросы, которые либо некорректны, либо делают что-то немного неожиданное, например, вводят привязки, видимые из кода, использующего макросы.

Обратите внимание, что я Я не выражаю предпочтения ни одному из подходов к макросам: я провел большую часть своей жизни за написанием CL, и я очень доволен его макросистемой. Совсем недавно я написал больше Racket, и я также доволен его системой макросов, хотя мне труднее ее понять.

Наконец, вот вариант вашего макроса, который менее удивителен в использовании (почти весь шум в этом коде - проверка работоспособности, которую syntax-parse поддерживает в виде двух #:fail-when предложений):

(define-syntax (with-char-codes stx)
  (syntax-parse stx
    [(_ (v:id ...) form ...)
     #:fail-when (check-duplicate-identifier (syntax->list #'(v ...)))
     "duplicate name"
     #:fail-when (for/or ([name (syntax->list #'(v ...))])
                   (and (> (string-length (symbol->string
                                           (syntax->datum name)))
                           1)
                        name))
     "name too long"
     #'(let ([v (char->integer (string-ref (symbol->string 'v) 0))] ...)
         form ...)]))

А теперь

> (with-char-codes (a b)
    (+ a b))
195
3 голосов
/ 15 марта 2020

Ракетка имеет гигиенический c макрос. Рассмотрим:

(define-syntax-rule (or a b)
  (let ([a-val a]) 
    (if a-val a-val b)))

Тогда:

(let ([a-val 1])
  (or #f a-val))

будет примерно расширяться до:

(let ([a-val 1])
  (let ([a-val2 #f]) 
    (if a-val2 a-val2 a-val)))

, что оценивается как 1. Если макрос не гигиенический c, то он приведет к #f, что считается неверным.

Обратите внимание, что a-val переименовывается в a-val2 автоматически, чтобы избежать столкновения. Это то же самое, что и с вашим делом.

Один из способов решения проблемы в вашем случае - дать правильный контекст сгенерированным идентификаторам, чтобы макроэкспандер понимал, что они должны ссылаться на одну и ту же переменную.

(define-syntax (test stx)
  (syntax-case stx ()
    [(_ body)
     #`(let #,(map (lambda (x)
                     (list (datum->syntax stx x) ; <-- change here
                           (char->integer (car (string->list (symbol->string x))))))
                   '(a b))
         body)]))

(test (+ a b))
...