我在Clojure中使用宏并且对宏扩展有疑问。在repl中,当我这样做时:
user=> (defmacro unless [pred a b] `(if (not ~pred) ~a ~b))
#'user/unless
user=> (macroexpand-1 '(unless (> 5 3) :foo :bar))
(if (clojure.core/not (> 5 3)) :foo :bar)
但是当我在clj文件中做同样的事情时:
(ns scratch-pad.core
(:gen-class))
(defmacro unless [pred a b]
`(if (not ~pred) ~a ~b))
(defn -main [& args]
(prn
(macroexpand-1 '(unless (> 5 3) :foo :bar))))
并运行代码,我明白了:
$ lein run
(unless (> 5 3) :foo :bar)
如何让代码与repl一样打印?
答案 0 :(得分:6)
这是因为当前命名空间的概念在Clojure中的工作原理。 macroexpand-1
在当前命名空间中扩展其参数。
在REPL,这将是user
;你在user
命名空间中定义宏,然后在该命名空间中调用macroexpand-1
,一切都很好。
在:gen-class'd
命名空间或任何其他命名空间中,编译时当前命名空间就是该命名空间本身。但是,当您稍后调用此命名空间中定义的代码时,当时的命名空间将是该点适当的任何内容。这可能是一些其他命名空间,因为它被编译。
最后,在您的应用运行时,默认的当前命名空间为user
。
要看到这一点,您可以将宏移动到一个单独的命名空间,同时定义一个函数use-the-macro
并在顶层调用此函数;然后:gen-class
'命名空间需要或使用宏的命名空间。然后lein run
将打印您期望的一次(在宏的命名空间的编译时)和未展开的形式两次(当主命名空间的宏命名空间为require
时,然后-main
调用use-the-macro
)。
Clojure REPL使用binding
控制当前命名空间;你也可以这样做:
(binding [*ns* (the-ns 'scratchpad.core)]
(prn (macroexpand-1 ...)))
您还可以在-main
中使用syntax-quote而不是引用:
(defn -main [& args]
(prn (macroexpand-1 `...)))
^- changed this
当然,如果涉及unless
以外的符号,则必须确定它们是否应在输出中进行名称空间限定,并可能在其前面添加~'
。这是重点 - syntax-quote适用于生成大多数“与命名空间无关”的代码(除了方便的语法之外,这使得它非常适合编写宏)。
另一种可能的“修复”(在Clojure 1.5.1上测试)正在向in-ns
添加-main
来电:
(defn -main [& args]
(in-ns 'scratchpad.core)
(prn (macroexpand-1 '...)))
^- no change here this time
与binding
一样,这样您实际上可以在原始命名空间中扩展原始表单。