是“透明"的小程序可能吗? [英] Is a "transparent" macrolet possible?

查看:20
本文介绍了是“透明"的小程序可能吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想编写一个 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,但由于每个宏只返回一个扩展,它的绑定不会在下一次调用 macroexpand 时就位.

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)))))

设计说明:

  1. 我们想要制作基于 macrolet 的设计问题文本工作.我们关心能够嵌套with-test-tags 并保留定义测试的可能性其主体靠近它们所定义的词汇环境在.

  1. We want to make the macrolet-based design described in the question text work. We care about being able to nest with-test-tags and preserving the possibility of defining tests whose bodies close over the lexical environments they are defined in.

我们将 macroletting deftest 扩展为clojure.test/deftest 附有适当元数据的表单测试名称.这里的重要部分是 with-test-tags将适当的标签集注入到定义中macrolet 表单中的自定义本地deftest;一旦编译器开始扩展 deftest 形式,标签集将被硬连接到代码中.

We will be macroletting 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-tagsmacrolet 符号 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 形式,带有 deftestwith-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:

  1. 动态绑定的 Var -- *tags* -- 在编译时绑定deftest 表单使用的一组标记来装饰测试被定义​​.我们默认不添加标签,所以它的初始值为 #{}.

  1. A dynamically bindable Var -- *tags* -- which is bound at compile time to a set of tags to be used by deftest 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))

是提供给自定义的参数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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆