是“透明"的吗?宏可能吗? [英] Is a "transparent" macrolet possible?
问题描述
我想编写一个 Clojure with-test-tags
宏来包装一堆表单,并在每个 deftest
表单的名称中添加一些元数据 -具体来说,在 :tags
键中添加一些东西,这样我就可以使用一个工具来运行带有特定标签的测试.
I'd like to write a Clojure with-test-tags
macro that wraps a bunch of forms, and adds some metadata to the name of each deftest
form - specifically, add some stuff to a :tags
key, so that I can play with a tool to run tests with a specific tag.
with-test-tags
的一个明显实现是递归遍历整个主体,根据我找到的每个 deftest
表单进行修改.但是我最近一直在阅读 Let Over Lambda,他提出了一个很好的观点:与其自己走代码,不如将代码包装在 macrolet
中并让编译器为你走.比如:
One obvious implementation for with-test-tags
is to walk the entire body recursively, modifying each deftest
form as I find it. But I've been reading Let Over Lambda recently, and he makes a good point: instead of walking the code yourself, just wrap the code in a macrolet
and let the compiler walk it for you. Something like:
(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...)))
但是,这有一个明显的问题,即 deftest
宏会永远递归地扩展.我可以将其扩展为 clojure.test/deftest
,从而避免任何进一步的递归扩展,但是我不能有效地将 with-test-tags
的实例嵌套到标记测试的子组.
This has the obvious problem, though, that the deftest
macro continues to expand recursively forever. I could make it expand to clojure.test/deftest
instead, thus avoiding any further recursive expansions, but then I can't usefully nest instances of with-test-tags
to label sub-groups of tests.
此时,尤其是对于像 deftest
这样简单的东西,看起来自己走代码会更简单.但我想知道是否有人知道一种编写宏的技术,该技术可以稍微修改"某些子表达式,而不会永远递归.
At this point, especially for something as simple as deftest
, it looks like walking the code myself will be simpler. But I wonder if anyone knows a technique for writing a macro which "slightly modifies" certain subexpressions, without recursing forever.
出于好奇:我考虑了一些其他方法,例如在我上下代码时设置一个编译时 binding
-able var,并在我最终使用该 var查看 deftest
,但由于每个宏只返回一个扩展,因此它的绑定不会在下一次调用宏扩展时就位.
For the curious: I considered some other approaches, such as having a compile-time binding
-able var that I set as I go up and down the code, and using that var when I finally see a deftest
, but since each macro only returns a single expansion its bindings won't be in place for the next call to macroexpand.
我刚刚完成了 postwalk 实现,虽然它可以工作,但它不尊重诸如 quote
之类的特殊形式 - 它也在其中扩展.
I did the postwalk implementation just now, and while it works it doesn't respect special forms such as quote
- it expands inside of those as well.
(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)))
(另外,对于 common-lisp 标记上可能出现的噪音,我深表歉意–我认为即使您只有很少的 Clojure 经验,也可以帮助处理更奇怪的宏.)
(Also, sorry for possible noise on the common-lisp tag – I thought you might be able to help out with weirder macro stuff even with minimal Clojure experience.)
推荐答案
(这是一种新方法,eval
- 和 binding
-free.如前所述在关于这个答案的评论,使用 eval
是有问题的,因为它可以防止测试关闭它们看起来的词汇环境定义在 (so (let [x 1] (deftest easy (is (= x 1))))
否更长的作品).我将原始方法留在了下半部分答案,低于水平线.)
(This is a new approach, eval
- and binding
-free. As discussed in
the comments on this answer, the use of eval
is problematic because
it prevents tests from closing over the lexical environments they seem
to be defined in (so (let [x 1] (deftest easy (is (= x 1))))
no
longer works). I leave the original approach in the bottom half of the
answer, below the horizontal rule.)
使用 Clojure 1.3.0-beta2 测试;它可能应该与 1.2.x 一起使用好吧.
Tested with Clojure 1.3.0-beta2; it should probably work with 1.2.x as well.
(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)))
用法
...最好通过一组(通过)测试来证明:
Usage
...is best demonstrated with a suite of (passing) tests:
(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)))))
设计说明:
我们想要制作基于
macrolet
的设计问题文本工作.我们关心能够筑巢with-test-tags
并保留定义测试的可能性其主体封闭在它们被定义的词汇环境之上在.
We want to make the
macrolet
-based design described in the question text work. We care about being able to nestwith-test-tags
and preserving the possibility of defining tests whose bodies close over the lexical environments they are defined in.
我们将 macrolet
ting deftest
扩展成一个clojure.test/deftest
表单,附有适当的元数据测试的名称.这里重要的部分是 with-test-tags
将适当的标签集直接注入到macrolet
表单中的自定义本地 deftest
;一旦编译器开始扩展 deftest
形式,标签集将被硬连线到代码中.
We will be macrolet
ting deftest
to expand to a
clojure.test/deftest
form with appropriate metadata attached to
the test's name. The important part here is that with-test-tags
injects the appropriate tag set right into the definition of the
custom local deftest
inside the macrolet
form; once the
compiler gets around to expanding the deftest
forms, the tag sets
will have been hardwired into the code.
如果我们把它留在那里,测试定义在一个嵌套的with-test-tags
只会使用传递给最里面的 with-test-tags
表单.因此我们也有 with-test-tags
macrolet
符号 with-test-tags
本身的行为很像本地 deftest
:它扩展为对顶层的调用with-test-tags
宏,将适当的标签注入到标签集.
If we left it at that, tests defined inside a nested
with-test-tags
would only get tagged with the tags passed to the
innermost with-test-tags
form. Thus we have with-test-tags
also
macrolet
the symbol with-test-tags
itself behaving much like
the local deftest
: it expands to a call to the top-level
with-test-tags
macro with the appropriate tags injected into the
tagset.
本意是在
(with-test-tags #{:foo}
(with-test-tags #{:bar}
...))
扩展为 (deftest-magic.core/with-test-tags #{:foo :bar} ...)
(如果确实 deftest-magic.core
是命名空间 with-test-tags
定义在).这种形式立即扩展为熟悉的macrolet
形式,带有 deftest
和 with-test-tags
符号本地绑定到具有正确标签集的宏他们.
expand to (deftest-magic.core/with-test-tags #{:foo :bar} ...)
(if indeed deftest-magic.core
is the namespace with-test-tags
is defined in). This form immediately expands into the familiar
macrolet
form, with the deftest
and with-test-tags
symbols
locally bound to macros with the correct tag sets hardwired inside
them.
<小时>
(原始答案更新了一些关于设计的注释,一些改写和重新格式化等.代码不变.)
(有关版本,另请参阅 https://gist.github.com/1185513另外使用 macrolet
来避免自定义顶级deftest
.)
(See also https://gist.github.com/1185513 for a version
additionally using macrolet
to avoid a custom top-level
deftest
.)
以下内容经过测试可与 Clojure 1.3.0-beta2 一起使用;与^:dynamic
部分已删除,它应该适用于 1.2:
The following is tested to work with Clojure 1.3.0-beta2; with the
^:dynamic
part removed, it should work with 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#)))
用法
(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))))
设计说明
我认为在这种情况下,明智地使用 eval
会导致有用的解决方案.基本设计(基于binding
-able Var"想法)具有三个组成部分:
Design notes
I think that on this occasion a judicious use of eval
leads to a
useful solution. The basic design (based on the "binding
-able Var"
idea) has three components:
一个可动态绑定的 Var --
*tags*
-- 在编译时绑定时间来一组标签被deftest
表单用来装饰正在定义的测试.我们默认不添加标签,所以它的初始值为#{}
.
A dynamically bindable Var --
*tags*
-- which is bound at compile time to a set of tags to be used bydeftest
forms to decorate the tests being defined. We add no tags by default, so its initial value is#{}
.
一个 with-test-tags
宏,它安装了一个合适的*tags*
.
A with-test-tags
macro which installs an appropriate for
*tags*
.
一个自定义的 deftest
宏,它扩展为类似于 let
的形式this(以下是展开,稍微简化一下清晰度):
A custom deftest
macro which expands to a let
form resembling
this (the following is the expansion, slightly simplified for
clarity):
(let [n (vary-meta '<NAME> update-in [:tags] (fnil into #{}) *tags*)
form (list* 'clojure.test/deftest n '<BODY>)]
(eval form))
<NAME>
和 <BODY>
是给自定义的参数deftest
,通过取消引用插入到适当的位置语法引用的扩展模板的适当部分.
<NAME>
and <BODY>
are the arguments given to the custom
deftest
, inserted in the appropriate spots through unquoting the
appropriate parts of the syntax-quoted expansion template.
因此自定义deftest
的扩展是一个let
形式,其中,首先,新测试的名称是通过装饰给定的带有 :tags
元数据的符号;然后是 clojure.test/deftest
表单使用这个修饰名被构造;最后是后一种形式交给eval
.
Thus the expansion of the custom deftest
is a let
form in which,
first, the name of the new test is prepared by decorating the given
symbol with the :tags
metadata; then a clojure.test/deftest
form
using this decorated name is constructed; and finally the latter form
is handed to eval
.
这里的重点是这里的(eval form)
表达式是每当它们包含的命名空间是 AOT 编译或在运行它的 JVM 的生命周期中第一次需要代码.这与 (println "asdf")
完全相同顶层 (def asdf (println "asdf"))
,将打印 asdf
每当命名空间被 AOT 编译或第一次需要 时间;事实上,顶级 (println "asdf")
的作用类似.
The key point here is that the (eval form)
expressions here are
evaluated whenever the namespace their contained in is AOT-compiled or
required for the first time in the lifetime of the JVM running this
code. This is exactly the same as the (println "asdf")
in a
top-level (def asdf (println "asdf"))
, which will print asdf
whenever the namespace is AOT-compiled or required for the first
time; in fact, a top-level (println "asdf")
acts similarly.
这可以通过注意到在 Clojure 中的编译只是评估所有顶级表格.在 (binding [...] (deftest ...)
,binding
是最顶层的表单,但只有在 deftest
时才返回确实如此,并且我们的自定义 deftest
扩展为一个表单,当eval
可以.(另一方面,require
执行顶层的方式已经编译的命名空间中的代码——所以如果你有 (def t(System/currentTimeMillis))
在您的代码中,t
的值将取决于您何时需要命名空间,而不是何时需要已编译,可以通过使用 AOT 编译的代码进行试验来确定-- 这正是 Clojure 的工作方式.如果你想要实际的,请使用 read-eval嵌入在代码中的常量.)
This is explained by noting that compilation, in Clojure, is just
evaluation of all top-level forms. In (binding [...] (deftest ...)
,
binding
is the top-level form, but it only returns when deftest
does, and our custom deftest
expands to a form which returns when
eval
does. (On the other hand, the way require
executes top-level
code in already-compiled namespaces -- so that if you have (def t
(System/currentTimeMillis))
in your code, the value of t
will
depend on when you require your namespace rather than on when it was
compiled, as can be determined by experimenting with AOT-compiled code
-- is just the way Clojure works. Use read-eval if you want actual
constants embedded in code.)
实际上,自定义 deftest
运行编译器(通过 eval
)宏扩展的运行时编译时.有趣.
In effect, the custom deftest
runs the compiler (through eval
) at
the run-time-at-compile-time of macro expansion. Fun.
最后,当 deftest
表单放入 with-test-tags
表单时,(eval form)
的 form
将已准备好绑定由 with-test-tags
安装到位.因此定义了测试将使用适当的标签集进行装饰.
Finally, when a deftest
form is put inside a with-test-tags
form,
the form
of (eval form)
will have been prepared with the bindings
installed by with-test-tags
in place. Thus the test being defined
will be decorated with the appropriate set of 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
为了确保正在定义工作测试...
And just to be sure working tests are being defined...
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}
这篇关于是“透明"的吗?宏可能吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!