是“透明的”大小可能? [英] Is a "transparent" macrolet possible?
问题描述
我想写一个包含一堆表单的Clojure with-test-tags
宏,并将一些元数据添加到每个 deftest
form - 具体来说,向:tags
键添加一些东西,以便我可以使用工具来运行测试
with-test-tags
的一个显而易见的实现是递归遍历整个身体, code> deftest 形式,我发现它。但我最近一直在阅读 Let Over Lambda ,他提出了一个好点:而不是自己走代码,只需将代码包装在宏let
让编译器为你走。例如:
(defmacro with-test-tags [tags& body]
` 'deftest [name#& more#]
`(〜'〜'deftest〜(vary-meta name#update-in [:tags](fnil into [])〜tags)
〜@ more#))]
(do〜@ body)))
(with-test-tags [:a:b]
(deftest x ...)))
这有一个明显的问题, > deftest 宏继续以递归方式永久扩展。我可以让它扩展到 clojure.test / deftest
而不是,因此避免任何进一步的递归扩展,但是我不能有用地嵌套的实例-test-tags
来标记测试的子组。
此时,特别是对于 deftest
,看起来像走自己的代码会更简单。但是我想知道是否有人知道一种编写宏的方法,它稍微修改某些子表达式,而不会永远递归。
绑定
-able var,我设置为我上下移动代码,并使用该var,当我终于看到一个 deftest
,但由于每个宏仅返回一个扩展,其绑定将不会在下一次调用macroexpand。 编辑
我刚刚做了postwalk实现,虽然它工作,它不尊重特殊的形式,如 quote
它也在这些内部扩展。
(defmacro with-test-tags [tags& body]
cons $ do
(postnode)($ f $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ (第一种形式)))
(seq(update-in(vec form)[1]
vary-meta update-in [:tags](fnil into [])tags) b form))
body)))
common-lisp标签–我认为你可能能够帮助我们的weirder宏东西,即使有最小的Clojure经验。)
是一种新方法, eval
- 和 binding
-free。 ,使用 eval
是有问题的,因为
它阻止测试关闭它们看起来的词法环境
在((let [x 1](deftest easy(is(= x 1))))
no
更长的工作)我离开原来的方法在$ b
宏小
方法
实施
使用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# })〜标签)
'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
tags〜'tags)〜'body))]
`(biglet [〜deftest-decl
〜with-test-tags-decl]
〜
使用
... (传递)测试:
(ns deftest-magic.test.core
(:use [deftest-magic .core:only [with-test-tags]])
(:use clojure.test))
;;定义没有附加标签的测试:
(deftest plain-deftest
(is(=:foo:foo)))
(with-test-tags#{:foo }
;;这个测试将被标记为#{:foo}:
(deftest foo
(是true))
-tags#{:bar}
;;这个测试将被标记为#{:foo:bar}:
(deftest foo-bar
(is true)))
;;确认在上面的注释中提出的声明:
(deftest test-tags
(let [plaintest-tags(:tags(meta#'plain-deftest))]
nil?plaintest-tags)(empty?plaintest-tags)))
(is(=#{:foo} {:foo:bar}(:tags(meta#'foo-bar)))))
;;测试可以是关闭:
(let [x 1]
(deftest lexical-bindings-no-tags
(is(= x 1))))
;;这个工作在内部with-test-args中:
(with-test-tags#{:foo}
(let [x 1]
(deftest easy(is true))
(deftest lexical-bindings-with-tags
(is(=#{:foo}(:tags(meta#'easy))))
(is(= x 1))))) )
设计说明:
-
我们想让
问题文本中描述的基于宏let
的设计工作。我们关心的是能够嵌套
with-test-tags
并保留定义测试的可能性
,它们的主体靠近它们定义的词法环境
in。 -
我们将
macrolet
tingdeftest
展开成一个
clojure.test / deftest
表单,并附加适当的元数据到
测试名称。这里的重要部分是with-test-tags
将适当的标记集注入到
定义中的自定义deftest
在宏let
中;一旦
编译器扩展了deftest
表单,标记集
将被硬连线到代码中。 -
如果我们离开它,在嵌套的
with-test-tags
传递给
最内层with-test-tags
表单的标签。因此,我们有with-test-tags
也
macrolet
符号test-tags
本身表现得很像
本地deftest
:它扩展为对顶级的调用
with-test-tags
宏将相应的标记注入
标记集。 -
意图是在
中的
with-test-tags
with-test-tags#{:foo}
(with-test-tags#{:bar}
...))
展开为
(deftest-magic.core / with-test-tags#{:foo:bar} ...)
(如果确实deftest-magic.core
是命名空间with-test-tags
定义在)。此表单立即扩展为熟悉的
宏let
表单,其中deftest
和with-test-tags
符号
在本地绑定到具有正确标记集的宏,
以内。
(原始答案更新了一些关于设计的注释,一些
重新格式化和重新格式化等。不变。)
绑定
+ eval $ c $
(另请参阅https://gist.github.com/1185513版本
另外使用 macrolet
以避免自定义顶级
deftest
。)
实现
以下内容经测试可与Clojure 1.3.0-beta2配合使用:除去
^:动态
部分,它应该与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#)))
h2>
(ns example.core
(:use [clojure.test:exclude [deftest]])
:use [deftest-magic.core:only [with-test-tags deftest]]))
;;用一组空标签定义一个测试:
(deftest no-tags
(是true))
(with-test-tags#{:foo}
;;这个测试将被标记为#{:foo}:
(deftest foo
(是true))
(with-test-tags# :bar}
;;这个测试将被标记为#{:foo:bar}:
(deftest foo-bar
(是true))))
(ns example.core
(:use [clojure.test:exclude [deftest]])
:use [deftest-magic.core:only [with-test-tags deftest]]))
;;用一组空标签定义一个测试:
(deftest no-tags
(是true))
(with-test-tags#{:foo}
;;这个测试将被标记为#{:foo}:
(deftest foo
(是true))
(with-test-tags# :bar}
;;这个测试将被标记为#{:foo:bar}:
(deftest foo-bar
(是true))))
设计笔记
使用 eval
导致一个
有用的解决方案。基本设计(基于 binding
-able Var
idea)有三个组件:
-
动态绑定Var -
* tags *
- 在compile
时绑定到一组标签用于deftest
表单来装饰正在定义的
测试。我们默认情况下不添加标签,因此其初始
值为#{}
。 -
A
with-test-tags
宏安装适用于
* tags *
> 形式类似
这个(下面是扩展,稍微简化为
清晰):[n(vary-meta'< NAME> update-in [:tags](fnil into#{})* tags *)
form(list *'clojure.test / deftest n'< BODY> ]
(eval form))
< NAME& / code>和
< BODY>
是赋予自定义
deftest
因此,通过不引用
的语法引用的扩展模板的适当部分,自定义 deftest
的扩展是一个 let
形式,其中,
第一,新的名称测试是通过用:tags
元数据装饰给定的
符号来准备的;然后构造使用这个装饰名的 clojure.test / deftest
形式
;最后将后面的形式
移交给 eval
。
code>(eval form)表达式是
,当它们包含的命名空间是AOT编译或
第一次需要 >在运行此
代码的JVM的生命周期中。这与
顶级(def asdf(printlnasdf))中的
,每当命名空间被AOT编译或需要时,它将打印(printlnasdf)
) asdf
;实际上,顶层(printlnasdf)
的行为类似。
编译,在Clojure中,只是
评价所有顶级窗体。在(绑定[...](deftest ...)
,
绑定
但它只会在 deftest
时返回,而我们的自定义 deftest
会扩展为一个窗体当
eval
时返回。(另一方面, require
执行顶级
代码在已编译的命名空间中 - 因此,如果你的代码中有(def t
c $ c> t 将
(System / currentTimeMillis))
取决于何时需要您的命名空间,而不是当它是
编译时,这可以通过实验AOT编译的代码$ b $如果你想在代码中嵌入实际的
常量,那么就是Clojure工作的方式。)
实际上,自定义 deftest
在
运行编译器(通过 eval
)宏扩展的编译时运行时。Fun。
最后,当 deftest
表单放在 -tags
form,
(eval form)
的形式
已准备好由 with-test-tags
安装的绑定
。
在REPL
user => (使用'deftest-magic.core'[clojure.test:exclude [deftest]])
nil
user => (with-test-tags#{:foo}
(deftest foo(is true))
(with-test-tags#{:bar}
(deftest foo-bar ))))
#'user / foo-bar
user => (meta#'foo)
{:ns#< Namespace user>,
:name foo,
:fileNO_SOURCE_PATH,
:line 2,
:test#< user $ fn__90 user $ fn__90 @ 50903025>,
:tags#{:foo}}; < =请注意标签
user => (meta#'foo-bar)
{:ns#< Namespace user>,
:name foo-bar,
:fileNO_SOURCE_PATH,
:
:test#< user $ fn__94 user $ fn__94 @ 368b1a4f>,
:tags#{:foo:bar}}; < = similar
user => (deftest quux(is true))
#'user / quux
user => (meta#'quux)
{:ns#< Namespace user>,
:name quux,
:fileNO_SOURCE_PATH,
:line 5,
:test#< user $ fn__106 user $ fn__106 @ b7c96a9>,
:tags#{}}; < =没有标签工作太多
只是为了确保正在定义工作测试...
user => (运行测试用户)
测试用户
包含3个断言的3个测试。
0失败,0错误。
{:type:summary,:pass 3,:test 3,:error 0,:fail 0}
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.)
(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.)
The macrolet
approach
Implementation
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)))))
Design notes:
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.We will be
macrolet
tingdeftest
to expand to aclojure.test/deftest
form with appropriate metadata attached to the test's name. The important part here is thatwith-test-tags
injects the appropriate tag set right into the definition of the custom localdeftest
inside themacrolet
form; once the compiler gets around to expanding thedeftest
forms, the tag sets will 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 the innermostwith-test-tags
form. Thus we havewith-test-tags
alsomacrolet
the symbolwith-test-tags
itself behaving much like the localdeftest
: it expands to a call to the top-levelwith-test-tags
macro with the appropriate tags injected into the tagset.The intention is that the inner
with-test-tags
form in(with-test-tags #{:foo} (with-test-tags #{:bar} ...))
expand to
(deftest-magic.core/with-test-tags #{:foo :bar} ...)
(if indeeddeftest-magic.core
is the namespacewith-test-tags
is defined in). This form immediately expands into the familiarmacrolet
form, with thedeftest
andwith-test-tags
symbols locally 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-level
deftest
.)
Implementation
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#)))
Usage
(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))))
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:
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#{}
.A
with-test-tags
macro which installs an appropriate for*tags*
.A custom
deftest
macro which expands to alet
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>
and<BODY>
are the arguments given to the customdeftest
, inserted in the appropriate spots through unquoting the appropriate parts of the syntax-quoted expansion template.
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
.
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.
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.)
In effect, the custom deftest
runs the compiler (through eval
) at
the run-time-at-compile-time of macro expansion. Fun.
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.
At the REPL
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屋!