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

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

问题描述

我想编写一个 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)))))

设计说明:

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

<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屋!

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