如何以编程方式确定哪些 Var 可能会影响 Clojure 中定义的函数的结果?
考虑一下 Clojure 函数的定义:
(def ^:dynamic *increment* 3)
(defn f [x]
(+ x *increment*))
这是 的函数x
,也是*increment*
(以及clojure.core/+
(1)的函数;但我不太关心)。在为这个函数编写测试时,我想确保我控制所有相关的输入,所以我做了这样的事情:
(assert (= (binding [*increment* 3] (f 1)) 4))
(assert (= (binding [*increment* -1] (f 1)) 0))
(想象一下,这*increment*
是一个配置值,有人可能会合理地更改;我不希望这个函数的测试在发生这种情况时需要更改。)
我的问题是:我如何编写一个断言,它的值(f 1)
可以依赖于*increment*
但不依赖于任何其他 Var?因为我希望有一天有人会重构一些代码并导致函数
(defn f [x]
(+ x *increment* *additional-increment*))
并忽略更新测试,即使*additional-increment*
为零,我也希望测试失败。
这当然是一个简化的例子——在一个大型系统中,可以有很多动态变量,并且可以通过一长串函数调用来引用它们。即使f
调用引用 Var 的g
调用,该解决方案也需要工作。h
如果它不声称(with-out-str (prn "foo"))
依赖于,那就太好了*out*
,但这并不重要。如果被分析的代码调用eval
或使用 Java 互操作,当然所有的赌注都没有了。
我可以想到三类解决方案:
从编译器获取信息
我想编译器会扫描函数定义以获取必要的信息,因为如果我尝试引用一个不存在的 Var,它会抛出:
user=> (defn g [x] (if true x (+ *foobar* x))) CompilerException java.lang.RuntimeException: Unable to resolve symbol: *foobar* in this context, compiling:(NO_SOURCE_PATH:24)
请注意,这发生在编译时,无论有问题的代码是否会被执行。因此,编译器应该知道函数可能引用了哪些 Var,并且我希望能够访问该信息。
解析源代码并遍历语法树,并记录引用 Var 的时间
因为代码就是数据等等。我想这意味着调用
macroexpand
和处理每个 Clojure 原语以及它们采用的每种语法。这看起来很像一个编译阶段,如果能够调用编译器的某些部分,或者以某种方式将我自己的钩子添加到编译器中,那就太好了。检测 Var 机制,执行测试并查看哪些 Var 被访问
不像其他方法那样完整(如果在我的测试未能执行的代码分支中使用 Var 怎么办?)但这就足够了。我想我需要重新定义
def
以产生类似于 Var 但以某种方式记录其访问的东西。
(1) 实际上,如果您重新绑定,该特定功能不会改变+
;但是在 Clojure 1.2 中,您可以绕过该优化,(defn f [x] (+ x 0 *increment*))
然后您就可以使用(binding [+ -] (f 3))
. 在 Clojure 1.3 中,尝试重新绑定+
会引发错误。