在Clojure中如何从另一个命名空间调用私有宏?

4

在一个文件中给出以下内容:

(ns demo.first)
(defmacro ^:private private-macro [a] a)

在另一个文件中有以下内容:
(ns demo.second
  (:require [demo.first :as first]))
(first/private-macro 10)

在demo.second中调用私有宏会抛出异常:var: #'demo.first/private-macro is not public,这是我预期的。

现在,有没有一种方法可以使这个调用成功,而不需要将宏公开?

对于函数,我可以这样做:

(#'first/private-macro 10)

但是使用宏时,会抛出以下错误:Wrong number of args (1) passed to: first/private-macro
我想对这个私有宏进行单元测试,个人更喜欢使用私有元数据而不是impl命名空间。这就是为什么我希望有一个解决方案的原因。
谢谢。
更新:
我发现由于defmacro本身就是一个宏,所以它首先扩展成形式,创建符号和宏的变量,并将其元数据添加到其中。
因此:
(defmacro ^:private private-macro [a] a)

首先通过defmacro宏进行处理,扩展为:

(do
  (clojure.core/defn ^{:private true} private-macro
    ([&form &env a] a))
  (. (var ^{:private true} private-macro)
     ^{:line 487, :column 49}
     (setMacro))
  (var ^{:private true} private-macro))

如您所见,接下来的情况如下:
  1. 使用 defn 声明了一个名为 private-macro 的函数,并将其设置为私有。
  2. 该函数接受三个参数 [&form &env a]这就是当使用 #' 调用宏时出现错误参数数量(1)异常的原因。
  3. 通过调用其 setMacro 方法,将 private-macro 变量设置为宏。
  4. 返回 private-macro 变量。

本质上,发生的事情是,如果您调用由 private-macro 变量指向的函数,例如使用 (#'private-macro) 语法的情况下,实际上您正在调用上述函数,该函数接受三个参数。 如果您的宏本身需要多个参数,则该函数将接受2 + 您的宏的参数数量。

所以我仍然不知道如何调用私有宏:

起初,我认为使用 nil 来替换 &form&env 可以解决问题:

(#'first/private-macro nil nil 10)

我的简单宏确实可以返回结果10。但是对于需要进一步扩展的复杂宏,它就会将宏展开并返回给我?!?
然后我想我可以使用alter-meta!在调用宏之前暂时删除宏中的私有元数据。具体操作如下:
(alter-meta! #'first/private-macro
             (fn [meta] (dissoc meta :private)))
(first/private-macro 10)
(alter-meta! #'first/private-macro
             (fn [meta] (assoc meta :private true)))

但是这仅在REPL中有效。试图在编译代码后运行,似乎编译器本身会抛出错误“var: #'demo.first/private-macro is not public”,即使在alter-meta!有机会运行之前,因此无法通过编译。
我真的不知道为什么#'不能像普通调用宏一样工作,以及为什么将nil传递给&form&env对于所有宏都不起作用。 如何在编译时使alter-meta!生效。 因此,如果有人知道,请回答!

也许Jay Fields可以帮助你更好地理解这些特殊的宏变量(&form和&env):http://blog.jayfields.com/2011/02/clojure-and.html - Chen Fisher
1个回答

2
对于我上面简单的宏,它可以返回10。但是对于更复杂的宏,需要进一步扩展,它就不行了,而是返回了宏展开的代码?!是的。正如你发现的那样,当你编写(defmacro m [x] (list x x))时,你:1.定义了一个名为m的函数,该函数将表单作为输入并生成表单作为输出;2.告诉编译器查找类似(m a)的调用,并在编译时用调用您的m函数的结果替换它们。通过调用#'m,你绕过了第二步:没有调用宏m,所以编译器不会在编译时调用它或用结果替换调用代码。由于#'m只是一个常规函数,它以代码作为输入并生成代码,当您绕过特殊的编译器行为并在运行时调用它时,您当然会得到代码作为结果(因为它已经是运行时)。好消息是:几乎没有必要使宏成为私有的,因为让其他命名空间调用它不会造成任何伤害。所有私有宏所做的就是将客户端本来可以手动编写的代码展开。因此,如果您控制此宏,您应该将其公开。如果您不能控制,则可以编写宏为您编写宏的代码。如果您绝对坚持调用别人的私有宏,那么可以将第(1)和第(2)部分拆分:定义自己的宏,其实现委托到另一个命名空间中支持私有变量的函数。
(defmacro cheat [& args]
  (apply #'m &form &env args))

由于cheat是您自己的宏,因此您可以以通常的方式调用它,利用编译器的“在编译时调用此代码”的机制。然后您可以将其委托给生成所需代码的函数,显式传递&form&env


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