5

我在 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 相同?

4

1 回答 1

6

这是怎么回事

这是因为当前命名空间的概念在 Clojure 中是如何工作的。macroexpand-1在当前命名空间中展开它的参数。

在 REPL,这将是user; 您在命名空间中定义宏user,然后在该命名空间中调用macroexpand-1,一切都很好。

:gen-class'd命名空间或任何其他命名空间中,编译时的当前命名空间就是该命名空间本身。但是,当您稍后调用此命名空间中定义的代码时,当时的命名空间将是此时合适的任何名称。这可能是编译时的其他命名空间。

最后,在您的应用程序运行时,默认的当前命名空间是user.

要看到这一点,您可以将宏移动到单独的命名空间,同时定义一个函数use-the-macro并在顶层调用此函数;'d 命名空间将:gen-class需要或使用宏的命名空间。然后lein run将打印您期望的一次(在宏的命名空间的编译时)和两次未扩展的形式(当宏的命名空间是require主命名空间时,然后是-main调用时use-the-macro)。

解决方案

bindingClojure REPL 使用;控制当前命名空间。你也可以做到的:

(binding [*ns* (the-ns 'scratchpad.core)]
  (prn (macroexpand-1 ...)))

您还可以使用 syntax-quote 代替 quote in -main

(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,通过这种方式,您实际上可以在原始命名空间中扩展原始表单。

于 2013-05-12T23:12:17.137 回答