ama*_*loy 15 macros clojure common-lisp
我想编写一个with-test-tags包含大量表单的Clojure 宏,并在每个deftest表单的名称中添加一些元数据- 具体来说,在:tags键中添加一些东西,这样我就可以使用工具来运行具体的测试标签.
一个明显的实现with-test-tags是递归地遍历整个身体,deftest在我找到它时修改每个表单.但是我最近一直在阅读Let Over Lambda,他提出了一个很好的观点:不要自己编写代码,只需将代码包装好,macrolet然后让编译器为你完成.就像是:
(defmacro with-test-tags [tags & body]
`(macrolet [(~'deftest [name# & more#]
`(~'~'deftest ~(vary-meta name# update-in [:tags] (fnil into []) ~tags)
~@more#))]
(do ~@body)))
(with-test-tags [:a :b]
(deftest x (...do tests...)))
Run Code Online (Sandbox Code Playgroud)
但是,这有一个明显的问题,即deftest宏继续递归地递增.我可以将它扩展为clojure.test/deftest相反,从而避免任何进一步的递归扩展,但是我无法有效地嵌套with-test-tags标记子组测试的实例.
在这一点上,特别是对于简单的事情deftest,它看起来像走自己的代码会更简单.但我想知道是否有人知道编写宏的技术,它会"略微修改"某些子表达式,而不会永远递归.
对于好奇:我考虑了一些其他的方法,例如我有一个编译时binding可变的var,我设置为上下代码,并在我最终看到a时使用该var deftest,但由于每个宏只返回一个扩展它的绑定将不适用于下一次调用macroexpand.
我刚刚做了postwalk实现,虽然它工作但它不尊重特殊形式,例如quote- 它也扩展到那些内部.
(defmacro with-test-tags [tags & body]
(cons `do
(postwalk (fn [form]
(if (and (seq? form)
(symbol? (first form))
(= "deftest" (name (first form))))
(seq (update-in (vec form) [1]
vary-meta update-in [:tags] (fnil into []) tags))
form))
body)))
Run Code Online (Sandbox Code Playgroud)
(另外,对于common-lisp标签上可能出现的噪音感到抱歉 - 我认为即使只有最小的Clojure经验,你也可以帮助我们处理更奇怪的宏观内容.)
(这是一种新的方法,eval- 和binding- 免费.正如在这个答案的评论中讨论的那样,使用eval是有问题的,因为它可以防止测试关闭它们似乎被定义的词汇环境(因此(let [x 1] (deftest easy (is (= x 1))))不再有效).将原始方法保留在答案的下半部分,低于横向规则.)
macrolet方法用Clojure 1.3.0-beta2测试; 它应该也适用于1.2.x.
(ns deftest-magic.core
(:use [clojure.tools.macro :only [macrolet]]))
(defmacro with-test-tags [tags & body]
(let [deftest-decl
(list 'deftest ['name '& 'body]
(list 'let ['n `(vary-meta ~'name update-in [:tags]
(fnil into #{}) ~tags)
'form `(list* '~'clojure.test/deftest ~'n ~'body)]
'form))
with-test-tags-decl
(list 'with-test-tags ['tags '& 'body]
`(list* '~'deftest-magic.core/with-test-tags
(into ~tags ~'tags) ~'body))]
`(macrolet [~deftest-decl
~with-test-tags-decl]
~@body)))
Run Code Online (Sandbox Code Playgroud)
...最好用一套(通过)测试来证明:
(ns deftest-magic.test.core
(:use [deftest-magic.core :only [with-test-tags]])
(:use clojure.test))
;; defines a test with no tags attached:
(deftest plain-deftest
(is (= :foo :foo)))
(with-test-tags #{:foo}
;; this test will be tagged #{:foo}:
(deftest foo
(is true))
(with-test-tags #{:bar}
;; this test will be tagged #{:foo :bar}:
(deftest foo-bar
(is true))))
;; confirming the claims made in the comments above:
(deftest test-tags
(let [plaintest-tags (:tags (meta #'plain-deftest))]
(is (or (nil? plaintest-tags) (empty? plaintest-tags))))
(is (= #{:foo} (:tags (meta #'foo))))
(is (= #{:foo :bar} (:tags (meta #'foo-bar)))))
;; tests can be closures:
(let [x 1]
(deftest lexical-bindings-no-tags
(is (= x 1))))
;; this works inside with-test-args as well:
(with-test-tags #{:foo}
(let [x 1]
(deftest easy (is true))
(deftest lexical-bindings-with-tags
(is (= #{:foo} (:tags (meta #'easy))))
(is (= x 1)))))
Run Code Online (Sandbox Code Playgroud)
我们希望使macrolet问题文本中描述的基于设计的设计发挥作用.我们关心的是能够嵌套
with-test-tags并保留定义测试的可能性,这些测试的主体靠近它们所定义的词汇环境.
我们将以macrolet婷deftest扩大到
clojure.test/deftest与连接到测试的名称适当的元数据的形式.这里的重要部分是with-test-tags
将适当的标记集注入到表单deftest内自定义local的定义中macrolet; 一旦编译器开始扩展deftest表单,标记集就会硬连接到代码中.
如果我们把它留在那里,嵌套内定义的测试
with-test-tags只会被标记为传递给最里面with-test-tags形式的标签.因此,我们with-test-tags也
macrolet有符号with-test-tags本身的行为很像本地deftest:它扩展为调用顶级
with-test-tags宏,并在标记集中注入适当的标记.
意图是内在的with-test-tags形式
(with-test-tags #{:foo}
(with-test-tags #{:bar}
...))
Run Code Online (Sandbox Code Playgroud)
扩展到(deftest-magic.core/with-test-tags #{:foo :bar} ...)
(如果确实deftest-magic.core是命名空间with-test-tags
定义).此表单立即扩展为熟悉的
macrolet形式,其中deftest和with-test-tags符号本地绑定到宏,其中正确的标记集在其中硬连线.
(原始答案更新了一些关于设计的注释,一些改写和重新格式化等.代码没有改变.)
binding+ eval的办法.(另请参阅https://gist.github.com/1185513以获取另一个macrolet用于避免自定义顶级
版本的版本deftest.)
以下测试与Clojure 1.3.0-beta2一起使用; 与
^:dynamic删除的一部分,它应与1.2的工作:
(ns deftest-magic.core)
(def ^:dynamic *tags* #{})
(defmacro with-test-tags [tags & body]
`(binding [*tags* (into *tags* ~tags)]
~@body))
(defmacro deftest [name & body]
`(let [n# (vary-meta '~name update-in [:tags] (fnil into #{}) *tags*)
form# (list* 'clojure.test/deftest n# '~body)]
(eval form#)))
Run Code Online (Sandbox Code Playgroud)
(ns example.core
(:use [clojure.test :exclude [deftest]])
(:use [deftest-magic.core :only [with-test-tags deftest]]))
;; defines a test with an empty set of tags:
(deftest no-tags
(is true))
(with-test-tags #{:foo}
;; this test will be tagged #{:foo}:
(deftest foo
(is true))
(with-test-tags #{:bar}
;; this test will be tagged #{:foo :bar}:
(deftest foo-bar
(is true))))
Run Code Online (Sandbox Code Playgroud)
我认为,在这种情况下,明智地使用eval会导致有用的解决方案.基本设计(基于" binding-able Var"的想法)有三个组成部分:
一个动态可绑定的Var - *tags*- 在编译时绑定到一组标签,供deftest表单用来装饰正在定义的测试.我们默认不添加任何标签,因此其初始值为#{}.
甲with-test-tags其中安装一个适当的宏
*tags*.
一个自定义deftest宏,它扩展为let类似于此的形式(以下是扩展,为了清晰起见略微简化):
(let [n (vary-meta '<NAME> update-in [:tags] (fnil into #{}) *tags*)
form (list* 'clojure.test/deftest n '<BODY>)]
(eval form))
Run Code Online (Sandbox Code Playgroud)
<NAME>并且<BODY>是给定制的参数
deftest,通过取消引用语法引用的扩展模板的相应部分插入适当的位置.
因此,自定义的扩展deftest是这样一种let形式,其中,首先,通过用:tags元数据装饰给定符号来准备新测试的名称; 然后clojure.test/deftest构造一个使用这个装饰名称的表单; 最后,后一种形式交给了eval.
这里的关键点是,(eval form)只要它们包含的命名空间是AOT编译的,或者在运行此代码的JVM的生命周期中第一次需要它们,就会对这里的表达式进行求值.这与(println "asdf")顶层完全相同(def asdf (println "asdf")),asdf
只要命名空间是AOT编译或第一次需要,它就会打印出来; 事实上,顶级(println "asdf")行为同样如此.
这可以通过注意到Clojure中的编译只是对所有顶级表单的评估来解释.在(binding [...] (deftest ...),
binding顶级表单,但它只返回时deftest
,我们的自定义deftest扩展eval为何时返回的表单
.(另一方面,这种方式require在已经编译的命名空间中执行顶级代码 - 因此,如果你(def t
(System/currentTimeMillis))的代码中有值,那么t取决于你需要命名空间的时间而不是编译时的值,就像通过试验AOT编译的代码来确定 - 只是Clojure的工作方式.如果你想在代码中嵌入实际的常量,请使用read-eval.)
实际上,自定义在宏扩展的运行时编译时deftest运行编译器(直通eval).乐趣.
最后,当一个deftest形式把里面with-test-tags的形式,对form中(eval form)会已准备与安装绑定with-test-tags到位.因此,定义的测试将使用适当的标签集进行修饰.
user=> (use 'deftest-magic.core '[clojure.test :exclude [deftest]])
nil
user=> (with-test-tags #{:foo}
(deftest foo (is true))
(with-test-tags #{:bar}
(deftest foo-bar (is true))))
#'user/foo-bar
user=> (meta #'foo)
{:ns #<Namespace user>,
:name foo,
:file "NO_SOURCE_PATH",
:line 2,
:test #<user$fn__90 user$fn__90@50903025>,
:tags #{:foo}} ; <= note the tags
user=> (meta #'foo-bar)
{:ns #<Namespace user>,
:name foo-bar,
:file "NO_SOURCE_PATH",
:line 2,
:test #<user$fn__94 user$fn__94@368b1a4f>,
:tags #{:foo :bar}} ; <= likewise
user=> (deftest quux (is true))
#'user/quux
user=> (meta #'quux)
{:ns #<Namespace user>,
:name quux,
:file "NO_SOURCE_PATH",
:line 5,
:test #<user$fn__106 user$fn__106@b7c96a9>,
:tags #{}} ; <= no tags works too
Run Code Online (Sandbox Code Playgroud)
只是为了确保正在定义工作测试......
user=> (run-tests 'user)
Testing user
Ran 3 tests containing 3 assertions.
0 failures, 0 errors.
{:type :summary, :pass 3, :test 3, :error 0, :fail 0}
Run Code Online (Sandbox Code Playgroud)