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

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相同的代码打印输出?


我认为答案的讽刺版本是“修复Clojure中的错误并提交拉取请求”。该行为不一致,似乎没有记录或意图。Bug可在Clojure版本1.3、1.4、1.5中重现。 - noisesmith
1
不幸的是,无论是否讽刺,任何实际的拉取请求都将被忽略,我认为;相反,您必须签署CA(在实际纸张上)并通过Jira提交补丁--http://clojure.org/contributing - JohnJ
1个回答

6

发生了什么

这是因为在Clojure中,当前命名空间的概念如何工作而导致的。 macroexpand-1 在当前命名空间中扩展其参数。

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

:gen-class'd命名空间或任何其他命名空间中,编译时当前命名空间就是该命名空间本身。 但是,当您稍后调用在此命名空间中定义的代码时,当前命名空间将是在那个时间点上适当的任何其他命名空间。 随着编译过程的进行,可能会出现一些其他的命名空间。

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

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

解决方案

Clojure REPL使用binding控制当前命名空间; 您也可以这样做:

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

-main中,您也可以使用语法引用(syntax-quote)而不是引用(quote):

(defn -main [& args]
  (prn (macroexpand-1 `...)))
                      ^- changed this

当然,如果除了“unless”之外的符号也涉及到,您需要决定它们是否应该在输出中加上命名空间限定,并可能用“~'”作为前缀。这就是关键所在——语法引用适用于生成大多数“与命名空间无关”的代码(这也是编写宏非常好的原因之一,除了方便的语法)。
另一个可能的“修复”方法(在Clojure 1.5.1上测试)是在“-main”中添加一个“in-ns”调用:
(defn -main [& args]
  (in-ns 'scratchpad.core)
  (prn (macroexpand-1 '...)))
                      ^- no change here this time

绑定(binding)类似,这种方式实际上是在您的原始命名空间中获取原始表单的扩展。

1
当函数已经在该命名空间中时,为什么需要在函数中指定in-ns?这种行为非常令人困惑。此外,在没有使用任何unquote的情况下,`应该像'一样正常工作。我认为这是一个不错的解决方法,但潜在问题是一个错误。 - noisesmith
为什么更改命名空间会改变macroexpand / macroexpand-1 / clojure.walk/macroexpand-all的行为?不同命名空间有什么防止扩展的方法吗?另一个解决此问题的方法是:(macroexpand-1'(scratch-pad.core/unless ...))-在引用形式中完全限定使运行时扩展从另一个命名空间调用时正常工作 - noisesmith
不是特别指macroexpand-*,只是macroexpand-1使用常规机制来解析符号到变量,这取决于当前的命名空间(其他macroexpand委托给macroexpand-1)。 (这就是允许您在bar是当前命名空间中的变量或别名的名称时编写(defn foo [] (bar))而无需显式命名空间限定bar的原因。)同意您的解决方法将起作用;语法引用对表单中非引用部分的所有符号执行相同的操作(除了foo#,它是使用gensym的某种模式的速记)。 - Michał Marczyk
@Michal,根据你的第二个解决方案,这对我似乎有效:(defn -main [& args] (in-ns 'scratch-pad.core) (prn (macroexpand-1 \(unless (> 5 3) :foo :bar))))`。感谢你的回答。 - David Williams
编辑了答案以添加“binding”解决方案,并将引号移动到它们应该在的位置;同时重新表述了一些部分。 - Michał Marczyk
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接