是“透明的”大小可能? [英] Is a "transparent" macrolet possible?

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

问题描述

我想写一个包含一堆表单的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))))) )



设计说明:




  1. 我们想让
    问题文本中描述的基于宏let的设计工作。我们关心的是能够嵌套
    with-test-tags 并保留定义测试的可能性
    ,它们的主体靠近它们定义的词法环境
    in。


  2. 我们将 macrolet ting deftest 展开成一个
    clojure.test / deftest 表单,并附加适当的元数据到
    测试名称。这里的重要部分是 with-test-tags
    将适当的标记集注入到
    定义中的自定义 deftest 宏let中;一旦
    编译器扩展了 deftest 表单,标记集
    将被硬连线到代码中。


  3. 如果我们离开它,在嵌套的
    with-test-tags 传递给
    最内层 with-test-tags 表单的标签。因此,我们有 with-test-tags
    macrolet 符号 test-tags 本身表现得很像
    本地 deftest :它扩展为对顶级的调用
    with-test-tags 宏将相应的标记注入
    标记集。


  4. 意图是在

     中的 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

(另请参阅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))))



设计笔记



使用 eval 导致一个
有用的解决方案。基本设计(基于 binding -able Var
idea)有三个组件:


  1. 动态绑定Var - * tags * - 在compile
    时绑定到一组标签用于 deftest 表单来装饰正在定义的
    测试。我们默认情况下不添加标签,因此其初始
    值为#{}


  2. 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))中的(printlnasdf),每当命名空间被AOT编译或需要时,它将打印 asdf
;实际上,顶层(printlnasdf)的行为类似。



编译,在Clojure中,只是
评价所有顶级窗体。在(绑定[...](deftest ...)
绑定但它只会在 deftest
时返回,而我们的自定义 deftest 会扩展为一个窗体当
eval 时返回。(另一方面, require 执行顶级
代码在已编译的命名空间中 - 因此,如果你的代码中有(def t
(System / currentTimeMillis))
c $ c> t

取决于何时需要您的命名空间,而不是当它是
编译时,这可以通过实验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:

  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.

  2. 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.

  3. 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.

  4. 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 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.


(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:

  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 #{}.

  2. A with-test-tags macro which installs an appropriate for *tags*.

  3. 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> 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.

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

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