是“透明”的宏可以吗?
我想编写一个 Clojure with-test-tags
宏来包装一堆表单,并向每个 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...)))
但这有一个明显的问题,即 deftest 宏继续递归地永远扩展。我可以将其扩展为 clojure.test/deftest ,从而避免任何进一步的递归扩展,但随后我无法将 with-test-tags
的实例嵌套到标记测试子组。
在这一点上,特别是对于像 deftest 这样简单的事情,看起来我自己编写代码会更简单。但我想知道是否有人知道编写宏的技术,该技术“稍微修改”某些子表达式,而不会永远递归。
出于好奇:我考虑了一些其他方法,例如在代码中上下移动时设置一个编译时可绑定的变量,并在我最终看到 < code>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)))
(另外,对于 common-lisp 标签上可能出现的噪音,我深表歉意——我认为即使你的 Clojure 经验很少,你也可以帮助解决更奇怪的宏问题。)
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.
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...)))
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.
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.
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.
Edit
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)))
(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.)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
(这是一种新方法,
eval
- 和bounding
-free。正如中讨论的对此答案的评论,使用
eval
是有问题的,因为它阻止测试关闭它们看起来的词汇环境
定义在 (so
(let [x 1] (deftest easy (is (= x 1))))
否更长的作品)。我将原来的方法留在了下半部分
答案,位于水平规则下方。)
macrolet
方法实现
使用 Clojure 1.3.0-beta2 测试的 ;它可能应该与 1.2.x 一起工作
出色地。
用法
...最好通过一套(通过)测试来演示:
设计说明:
我们希望进行基于
macrolet
的设计,如问题文本工作。我们关心是否能够筑巢
with-test-tags
并保留定义测试的可能性他们的身体接近他们所定义的词汇环境
macrolet
tingdeftest
扩展为clojure.test/deftest
表单,附加适当的元数据测试的名称。这里重要的部分是
with-test-tags
将适当的标签集注入到定义中
macrolet
表单内自定义本地deftest
;一旦编译器开始扩展
deftest
形式,标签集将被硬连线到代码中。
如果我们就这样,在嵌套的内部定义测试
with-test-tags
只会使用传递给的标签进行标记最里面的
with-test-tags
形式。因此我们也有with-test-tags
macrolet
符号with-test-tags
本身的行为很像本地
deftest
:它扩展为对顶层的调用with-test-tags
宏,将适当的标签注入到taget。
其目的是内部
with-test-tags
形式扩展为
(deftest-magic.core/with-test-tags #{:foo :bar} ...)
(如果确实
deftest-magic.core
是命名空间with-test-tags
定义于)。这种形式立即扩展为熟悉的形式
macrolet
形式,带有deftest
和with-test-tags
符号本地绑定到宏,并在内部硬连线正确的标签集
(原始答案更新了一些设计注释,一些
重新措辞和重新格式化等。代码未更改。)
绑定
+eval
方法。(另请参阅 https://gist.github.com/1185513 了解版本
另外使用
macrolet
来避免自定义顶级deftest
.)实现
以下内容经过测试可与 Clojure 1.3.0-beta2 配合使用;与
^:dynamic
部分已删除,它应该适用于 1.2:使用
设计说明
我认为在这种情况下,明智地使用
eval
会导致有用的解决方案。基本设计(基于“
bounding
-able Var”)idea) 具有三个组件:
可动态绑定的 Var --
*tags*
-- 在编译时绑定deftest
表单使用一组标签来装饰的时间正在定义测试。我们默认不添加标签,所以它的初始值
值为
#{}
。一个
with-test-tags
宏,它安装了适当的*tags*
.自定义
deftest
宏,可扩展为类似于let
的形式这(以下是扩展,稍微简化为
清晰度):
和是赋予自定义的参数
deftest
,通过取消引用插入到适当的位置语法引用扩展模板的适当部分。
因此,自定义
deftest
的扩展是一个let
形式,其中,首先,通过装饰给定的来准备新测试的名称
带有
:tags
元数据的符号;然后是一个clojure.test/deftest
表单使用这个修饰名来构造;最后是后一种形式
被交给
eval
。这里的关键点是这里的
(eval form)
表达式是每当它们包含的命名空间是 AOT 编译的或
在运行此的 JVM 的生命周期中第一次需要
代码。这与 a 中的
(println "asdf")
完全相同顶级
(def asdf (println "asdf"))
,它将打印asdf
每当命名空间是 AOT 编译的或第一次需要时
时间;事实上,顶级
(println "asdf")
的作用类似。这是通过注意 Clojure 中的编译来解释的:
对所有顶级表格的评估。在
(绑定[...](deftest ...)
中,binding
是顶级表单,但仅在deftest
时返回确实如此,并且我们的自定义
deftest
扩展为一个表单,当eval
确实如此。 (另一方面,require
执行顶层的方式已经编译的命名空间中的代码——这样如果你有
(def t
在您的代码中,(System/currentTimeMillis))
t
的值将取决于您何时需要名称空间,而不是何时需要名称空间
已编译,可以通过试验 AOT 编译的代码来确定
——这就是 Clojure 的工作方式。如果你想要实际的,请使用 read-eval
嵌入代码中的常量。)
实际上,自定义
deftest
运行编译器(通过eval
)宏扩展的编译时运行时。乐趣。
最后,当将
deftest
表单放入with-test-tags
表单中时,(eval form)
的form
将已使用绑定准备好通过
with-test-tags
安装到位。因此测试被定义将用适当的标签集进行装饰。
在 REPL
并且只是为了确保正在定义工作测试......
(This is a new approach,
eval
- andbinding
-free. As discussed inthe comments on this answer, the use of
eval
is problematic becauseit prevents tests from closing over the lexical environments they seem
to be defined in (so
(let [x 1] (deftest easy (is (= x 1))))
nolonger works). I leave the original approach in the bottom half of the
answer, below the horizontal rule.)
The
macrolet
approachImplementation
Tested with Clojure 1.3.0-beta2; it should probably work with 1.2.x as
well.
Usage
...is best demonstrated with a suite of (passing) tests:
Design notes:
We want to make the
macrolet
-based design described in thequestion text work. We care about being able to nest
with-test-tags
and preserving the possibility of defining testswhose bodies close over the lexical environments they are defined
in.
We will be
macrolet
tingdeftest
to expand to aclojure.test/deftest
form with appropriate metadata attached tothe 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 themacrolet
form; once thecompiler gets around to expanding the
deftest
forms, the tag setswill have been hardwired into the code.
If we left it at that, tests defined inside a nested
with-test-tags
would only get tagged with the tags passed to theinnermost
with-test-tags
form. Thus we havewith-test-tags
alsomacrolet
the symbolwith-test-tags
itself behaving much likethe local
deftest
: it expands to a call to the top-levelwith-test-tags
macro with the appropriate tags injected into thetagset.
The intention is that the inner
with-test-tags
form inexpand to
(deftest-magic.core/with-test-tags #{:foo :bar} ...)
(if indeed
deftest-magic.core
is the namespacewith-test-tags
is defined in). This form immediately expands into the familiar
macrolet
form, with thedeftest
andwith-test-tags
symbolslocally bound to macros with the correct tag sets hardwired inside
them.
(The original answer updated with some notes on the design, some
rephrasing and reformatting etc. The code is unchanged.)
The
binding
+eval
approach.(See also https://gist.github.com/1185513 for a version
additionally using
macrolet
to avoid a custom top-leveldeftest
.)Implementation
The following is tested to work with Clojure 1.3.0-beta2; with the
^:dynamic
part removed, it should work with 1.2:Usage
Design notes
I think that on this occasion a judicious use of
eval
leads to auseful solution. The basic design (based on the "
binding
-able Var"idea) has three components:
A dynamically bindable Var --
*tags*
-- which is bound at compiletime to a set of tags to be used by
deftest
forms to decorate thetests being defined. We add no tags by default, so its initial
value is
#{}
.A
with-test-tags
macro which installs an appropriate for*tags*
.A custom
deftest
macro which expands to alet
form resemblingthis (the following is the expansion, slightly simplified for
clarity):
<NAME>
and<BODY>
are the arguments given to the customdeftest
, inserted in the appropriate spots through unquoting theappropriate parts of the syntax-quoted expansion template.
Thus the expansion of the custom
deftest
is alet
form in which,first, the name of the new test is prepared by decorating the given
symbol with the
:tags
metadata; then aclojure.test/deftest
formusing this decorated name is constructed; and finally the latter form
is handed to
eval
.The key point here is that the
(eval form)
expressions here areevaluated 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 atop-level
(def asdf (println "asdf"))
, which will printasdf
whenever the namespace is AOT-compiled or required for the first
time; in fact, a top-level
(println "asdf")
acts similarly.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 whendeftest
does, and our custom
deftest
expands to a form which returns wheneval
does. (On the other hand, the wayrequire
executes top-levelcode in already-compiled namespaces -- so that if you have
(def t
in your code, the value of(System/currentTimeMillis))
t
willdepend 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.)
In effect, the custom
deftest
runs the compiler (througheval
) atthe run-time-at-compile-time of macro expansion. Fun.
Finally, when a
deftest
form is put inside awith-test-tags
form,the
form
of(eval form)
will have been prepared with the bindingsinstalled by
with-test-tags
in place. Thus the test being definedwill be decorated with the appropriate set of tags.
At the REPL
And just to be sure working tests are being defined...
至少对于 Common Lisp,您只需为隐藏的宏添加别名即可。类似的东西:
Clojure 应该有类似的东西。该主题在这里讨论:定义 Clojure 宏的同义词。请注意,定义扩展为“deftest”调用的“deftest2”宏可能不起作用。
我发现这个答案有点晚了,但我还是把它贴在这里,供路人参考。
With Common Lisp at least, you simply alias the shadowed macro. Something like:
Clojure should have something similar. The topic is discussed here: define a synonym for a Clojure macro. Be aware that defining a 'deftest2' macro that expands to a 'deftest' call probably won't work.
I see that this answer is a little late, but I'll post it here for passers by.