命名空间混淆和宏

10

我想编写一个宏,使用来自clj-time库的函数。在一个命名空间中,我想这样调用宏:

(ns budget.account
  (:require [budget.time]))

(budget.time/next-date interval frequency)
下一个日期宏将在另一个文件中定义,如下所示:
(ns budget.time
  (:require [clj-time.core :as date]))

(defmacro next-date [interval freq]
  `(~interval ~freq))
如果使用以下参数(budget.time/next-date interval freq)调用宏,并且interval和freq分别是"weeks"和"2",那么宏展开看起来应该像这样(clj-time.core/weeks 2)。
每当我在REPL中尝试时,它无法解析命名空间。
是否有一种方法可以强制宏将interval解析为clj-time命名空间的参数?最好的方法是什么?
谢谢!

为什么需要编写宏?难道这不能通过常规函数完成吗?请记住:当函数可以胜任时,永远不要编写宏。 - viebel
1个回答

13
宏返回一个列表,然后在调用它的命名空间中进行评估,而不是在定义它的命名空间中进行评估。这与函数不同,函数在定义它们的命名空间中进行评估。这是因为宏返回要运行的代码,而不仅仅是运行它。
如果我进入另一个命名空间,例如hello.core,并展开对next-date的调用,则会得到:
hello.core> (macroexpand-1 '(next-date weeks 2))
(weeks 2) 

之后展开后,weeks从hello.core中解析出来,当然在那里它并没有定义。为了解决这个问题,我们需要返回的符号携带命名空间信息。

幸运的是,您可以使用ns-resolve显式地解析命名空间中的符号。它接受一个命名空间和一组符号,并尝试在该命名空间中找到它,如果未找到则返回nil。

(ns-resolve 'clj-time.core (symbol "weeks"))
#'clj-time.core/weeks

接下来你的宏将会获取一个符号和一个数字,这样我们可以不需要显式调用 symbol

(ns-resolve 'clj-time.core 'weeks)
#'clj-time.core/weeks

现在您只需要一个函数来解析该函数,然后创建一个由已解析的函数和数字组成的列表。

(defmacro next-date [interval freq]
  (list (ns-resolve 'clj-time.core interval) freq))
在上面的宏中,它所做的只是调用一个立即执行的函数,因此你甚至不需要一个宏来实现这个功能:
(defn next-date [interval freq]
  ((ns-resolve 'clj-time.core interval) freq))
(next-date 'weeks 2)
#<Weeks P2W>

非宏版本需要引用区间,因为在查找之前必须使其不被评估。此处宏的真正好处在于无需包含引用,代价是要求所有调用者都需要require clj-time。

当然,你也可以在所有地方都require clj-time,但这并不是重点。


1
在任何地方都要求使用clj-time,还是使用ns-resolve更好? - user1265564
1
就个人而言,我认为在任何地方都需要clj-time是更清晰的选择。 - Arthur Ulfeldt

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