Clojure如何在源文件和Repl中扩展宏

时间:2013-05-12 21:36:35

标签: macros clojure read-eval-print-loop

我在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一样打印?

1 个答案:

答案 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一样,这样您实际上可以在原始命名空间中扩展原始表单。