我试图充分理解编译时宏的局限性。
下面是一个宏(我完全知道这不是一个最佳实践宏):
(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,因此也从未被处理过。
为什么会这样呢?
在这个问题上,最好的读物是什么?
发布于 2022-02-25 16:43:55
为什么要用它代替?你从来没有取代过任何东西。
宏定义一个宏替换函数,该函数应用于代码中的实际表单,以生成另一个表单,然后进行编译。当您将宏定义应用于这些参数时,它将在宏展开时完成各种事情(编写文件等)。在返回princ返回的内容之前,这正是它的第一个参数,然后编译这个返回的表单。我不认为这是你想要的。
似乎你真正想要做的是扩展到一种形式,以一种不同的方式来解释身体,用第一个论点来表示。
您需要做的是返回新表单,以便
(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)即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)))注意,我引入了反勾符以创建一个模板,并引入逗号将外部参数放入其中。还请注意,参数是表单。
这有几个问题:宏的用户无法知道硬编码的符号。在一种情况下(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 )的“关于Lisp”是一本关于宏写作的书。彼得·塞贝尔( Peter )的“实用的通用Lisp”也有一个章节,还有一些菜谱在Edi的“公共Lisp”中。
发布于 2022-02-25 13:33:18
parenscript:ps是一个宏,而不是一个函数:它的主体是文字文本,从parenscript到JavaSctipt,它不是计算而是编译的。这一点很容易查证:
> (parenscript:ps body)
"body;"对于你应该读什么,我没有任何建议:这个宏看起来非常混乱,我无法真正理解它的根本意图是什么。CL中的宏是一个函数,其参数是某些语言L1中的源代码,并返回某些语言L2中的源代码,其中L2通常是L1的一个子集。但是,如果这只是一个人认为他们需要一个函数时需要宏的正常情况,或者是其他一些混乱的情况,我是无法解决的。
https://stackoverflow.com/questions/71261052
复制相似问题