宏参数未被替换

Macro argument not being substituted in

我正在尝试完全理解编译时宏的局限性。

这是一个宏(我完全知道这不是最佳实践宏):

(defmacro emit (language file &body body)
  (print language)
  (print file)
  (print body)
  (with-open-file (str file :direction :output :if-exists :supersede)
    (princ (cond ((eq language 'html)
                  (cl-who:with-html-output-to-string (s nil :prologue t :indent t) body))
                 ((eq language 'javascript)
                  (parenscript:ps body))
                 ((eq language 'json)
                  (remove #\; (parenscript:ps body))))
           str)))

我编译宏:

; processing (DEFMACRO EMIT ...)
PROGRAM> 

我编译这个表格:

PROGRAM> (compile nil (lambda () (emit json "~/file" (ps:create "hi" "hello") (ps:create "yo" "howdy"))))

JSON 
"~/file" 
((PARENSCRIPT:CREATE "hi" "hello") (PARENSCRIPT:CREATE "yo" "howdy")) 
#<FUNCTION (LAMBDA ()) {5367482B}>
NIL
NIL
PROGRAM> 

编译时 print 输出符合我的预期。

但是,如果我看 ~/file:

body

似乎 ((PARENSCRIPT:CREATE "hi" "hello") (PARENSCRIPT:CREATE "yo" "howdy")) 从未替换参数 body,因此从未被处理过。

这是为什么?

&关于这个主题的最佳文献是什么?

parenscript:ps 是一个宏,而不是一个函数:它的主体是字面的 parenscript 并且不是被评估而是被编译,从 Parenscript 到 JavaSctipt。这很容易检查:

> (parenscript:ps body)
"body;"

对于您应该阅读的内容,我没有任何建议:这个宏看起来非常混乱,我无法真正理解其潜在意图。 CL 中的宏是一个函数,其参数是某种语言 L1 中的源代码,并且 returns 某种语言 L2 中的源代码,其中 L2 通常是 L1 的子集。不过,我无法弄清楚,如果这只是正常情况下有人认为他们在需要一个函数时需要一个宏,或者如果这是其他一些混淆。

为什么要替代?你从来没有替换任何东西。

一个宏定义了一个宏替换函数,它被应用到代码中的实际形式以产生另一种形式,然后被编译。当您将宏定义应用于这些参数时,它将 在宏展开时 在 return 执行 princ 之前做各种事情(写入文件等) returned,这正是它的第一个参数,然后编译这个 returned 形式。我不认为那是你想要的。

看来您真正想要做的是扩展为一种形式,以多种方式之一解释 body,由第一个参数指示。

你需要做的是return新表格,这样

(emit 'html "foo.html"
  (:html (:head) (:body "whatever")))

扩展到

(with-open-file (str "foo.html" :direction :output :etc :etc)
  (cl-who:with-html-output (str)
    (:html (:head) (:body "whatever")))

为此,我们有一个模板语法:反引号。

`(foo ,bar baz)

的含义相同
(list 'foo bar 'baz)

但使转换后的代码结构更加清晰。还有,@拼接东西到一个列表里

`(foo ,@bar)

的含义相同
(list* 'foo bar)

我。 e. bar的内容,当它们是列表时,拼接成列表。这对于宏中的主体特别有用。

(defmacro emit (language file &body body)
  `(with-open-file (str ,file :direction :output :if-exists :supersede)
     (princ (cond ((eq ,language 'html)
                   (cl-who:with-html-output-to-string (s nil :prologue t :indent t)
                     ,@body))
                  ((eq ,language 'javascript)
                   (parenscript:ps ,@body))
                  ((eq ,language 'json)
                   (remove #\; (parenscript:ps ,@body))))
            str)))

请注意我在哪里引入了反引号来创建模板和逗号来将外部参数放入其中。还要注意参数是 forms.

这有几个问题:有宏用户无法知道的硬编码符号。在一种情况下 (str) 他们必须注意不要隐藏它,在另一种情况下 (s) 他们必须知道它才能写入它。为此,我们使用生成的符号(对于 str 这样就不会发生冲突)或者让用户说出他们想要命名的符号(对于 s)。此外,这个 cond 可以简化为 case:

(defmacro emit (language file var &body body)
  (let ((str (gensym "str")))
    `(with-open-file (,str ,file
                      :direction :output
                      :if-exists :supersede)
       (princ (case ,language
                ('html
                 (cl-who:with-html-output-to-string (,var nil
                                                     :prologue t
                                                     :indent t)
                   ,@body))
                ('javascript
                 (parenscript:ps ,@body))
                ('json
                 (remove #\; (parenscript:ps ,@body))))
              ,str)))

但是,您可能希望在宏展开时确定输出代码。

(defmacro emit (language file var &body body)
  (let ((str (gensym "str")))
    `(with-open-file (,str ,file
                      :direction :output
                      :if-exists :supersede)
       (princ ,(case language
                 ('html
                  `(cl-who:with-html-output-to-string (,var nil
                                                       :prologue t
                                                       :indent t)
                     ,@body))
                 ('javascript
                  `(parenscript:ps ,@body))
                 ('json
                  `(remove #\; (parenscript:ps ,@body))))
              ,str)))

在这里,您可以看到 case 形式已经在宏扩展时计算,然后使用内部模板创建内部形式。

这些都是完全未经测试的,所以删除小错误留作练习^^。

保罗·格雷厄姆 (Paul Graham) 的 »On Lisp« 是一本对宏编写有很多话要说的书。 Peter Seibel 的免费提供的 »Practical Common Lisp« 也有一章是关于它的,Edi Weitz 的 »Common Lisp Recipes« 中也有一些食谱。