&;Quot;透明&;;宏可能吗? [英] Is a "transparent" macrolet possible?
问题描述
我想编写一个Clojurewith-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
这样简单的东西,看起来我自己遍历代码会更简单。但我想知道,有没有人知道编写宏的技术,它可以"稍微修改"某些子表达式,而不会永远递归。
对于好奇的人:我考虑了一些其他方法,比如在代码上下移动时设置一个可在编译时binding
使用的变量,并在最终看到deftest
时使用该变量,但由于每个宏只返回一个扩展,因此它的绑定将不会用于下一次对宏扩展的调用。
编辑
我刚才做了Post Walk实现,虽然它可以工作,但它不支持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标记和ndash上可能出现的噪音,我很抱歉;我认为您可能能够帮助处理更奇怪的宏代码,即使只有最少的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)))
用法
...最好的演示是通过一套(通过)测试:
(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
并保留定义测试的可能性 他们的身体紧贴着他们所定义的词汇环境 在。- 我们将
macrolet
deftest
扩展到clojure.test/deftest
附加了适当元数据的表单 测试的名字。这里重要的部分是with-test-tags
将适当的标记集直接注入到macrolet
窗体内的自定义本地deftest
;一旦 编译器开始扩展deftest
表单、标记集 将被硬连接到代码中。 如果我们到此为止,在嵌套的 将仅使用传递给 最里面的
with-test-tags
形式。因此,我们也有with-test-tags
符号with-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
在中定义)。此表单立即扩展为熟悉的macrolet
形式,带有deftest
和with-test-tags
符号 本地绑定到内部具有正确标记集的宏 他们。/li>
(原始答案更新了一些关于设计的注释,一些 重新措辞和重新格式化等。代码保持不变。)
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#)))
用法
(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
-可用变量)
IDEA)有三个组成部分:
可动态绑定的变量--在编译时绑定 用于装饰
deftest
表单的一组标记的时间 正在定义测试。默认情况下,我们不添加任何标签,因此其初始 值为#{}
。with-test-tags
宏,该宏为*tags*
。
展开为类似以下形式的
deftest
自定义宏
这(下面是扩展,稍微简化了一下
清晰度):
(let [n (vary-meta '<NAME> update-in [:tags] (fnil into #{}) *tags*)
form (list* 'clojure.test/deftest n '<BODY>)]
(eval form))
<NAME>
和<BODY>
是提供给自定义
deftest
,通过反引号插入到适当的位置
语法引用的展开模板的适当部分。
:tags
元数据的符号;然后是clojure.test/deftest
表单
使用这个装饰的名字被构造;最后是后一种形式
交给eval
。
这里的关键点是这里的(eval form)
表达式是
只要它们包含的命名空间是AOT编译的或
在运行此命令的JVM的生存期内第一次需要
密码。这与
顶层(def asdf (println "asdf"))
,将打印asdf
只要命名空间是AOT编译的或第一个
时间;事实上,顶级(println "asdf")
的作用类似。
这可以通过注意Clojure中的编译只是
评估所有顶级表单。在[2-68]>中,
binding
是顶级表单,但仅当deftest
时才返回
,并且我们的自定义deftest
扩展为在以下情况下返回的表单
eval
。(另一方面,require
方式执行顶级
已编译的命名空间中的代码--因此,如果代码中有(def t
(System/currentTimeMillis))
,t
的值将
取决于何时需要命名空间,而不是何时需要命名空间
已编译,可以通过测试AOT编译的代码来确定
--这正是Clojure的工作方式。如果您想要Actual,请使用Read-Eval
代码中嵌入的常量。)
实际上,自定义deftest
在(通过eval
)处运行编译器
宏展开的编译时运行时。好玩。
最后,当deftest
表单放在with-test-tags
表单中时,
将使用绑定准备(eval form)
的form
由with-test-tags
原地安装。因此,测试被定义为
将使用适当的一组标记进行装饰。
在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
为了确保正在定义工作测试...
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}
这篇关于&;Quot;透明&;;宏可能吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!