"透明"的宏可能吗?

ama*_*loy 15 macros clojure common-lisp

我想编写一个with-test-tags包含大量表单的Clojure 宏,并在每个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...)))
Run Code Online (Sandbox Code Playgroud)

但是,这有一个明显的问题,即deftest宏继续递归地递增.我可以将它扩展为clojure.test/deftest相反,从而避免任何进一步的递归扩展,但是我无法有效地嵌套with-test-tags标记子组测试的实例.

在这一点上,特别是对于简单的事情deftest,它看起来像走自己的代码会更简单.但我想知道是否有人知道编写宏的技术,它会"略微修改"某些子表达式,而不会永远递归.

对于好奇:我考虑了一些其他的方法,例如我有一个编译时binding可变的var,我设置为上下代码,并在我最终看到a时使用该var deftest,但由于每个宏只返回一个扩展它的绑定将不适用于下一次调用macroexpand.

编辑

我刚刚做了postwalk实现,虽然它工作但它不尊重特殊形式,例如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)))
Run Code Online (Sandbox Code Playgroud)

(另外,对于common-lisp标签上可能出现的噪音感到抱歉 - 我认为即使只有最小的Clojure经验,你也可以帮助我们处理更奇怪的宏观内容.)

Mic*_*zyk 5

(这是一种新的方法,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)))
Run Code Online (Sandbox Code Playgroud)

用法

...最好用一套(通过)测试来证明:

(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)))))
Run Code Online (Sandbox Code Playgroud)

设计说明:

  1. 我们希望使macrolet问题文本中描述的基于设计的设计发挥作用.我们关心的是能够嵌套 with-test-tags并保留定义测试的可能性,这些测试的主体靠近它们所定义的词汇环境.

  2. 我们将以macroletdeftest扩大到 clojure.test/deftest与连接到测试的名称适当的元数据的形式.这里的重要部分是with-test-tags 将适当的标记集注入到表单deftest内自定义local的定义中macrolet; 一旦编译器开始扩展deftest表单,标记集就会硬连接到代码中.

  3. 如果我们把它留在那里,嵌套内定义的测试 with-test-tags只会被标记为传递给最里面with-test-tags形式的标签.因此,我们with-test-tagsmacrolet有符号with-test-tags本身的行为很像本地deftest:它扩展为调用顶级 with-test-tags宏,并在标记集中注入适当的标记.

  4. 意图是内在的with-test-tags形式

    (with-test-tags #{:foo}
      (with-test-tags #{:bar}
        ...))
    
    Run Code Online (Sandbox Code Playgroud)

    扩展到(deftest-magic.core/with-test-tags #{:foo :bar} ...) (如果确实deftest-magic.core是命名空间with-test-tags 定义).此表单立即扩展为熟悉的 macrolet形式,其中deftestwith-test-tags符号本地绑定到宏,其中正确的标记集在其中硬连线.


(原始答案更新了一些关于设计的注释,一些改写和重新格式化等.代码没有改变.)

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#)))
Run Code Online (Sandbox Code Playgroud)

用法

(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))))
Run Code Online (Sandbox Code Playgroud)

设计说明

我认为,在这种情况下,明智地使用eval会导致有用的解决方案.基本设计(基于" binding-able Var"的想法)有三个组成部分:

  1. 一个动态可绑定的Var - *tags*- 在编译时绑定到一组标签,供deftest表单用来装饰正在定义的测试.我们默认不添加任何标签,因此其初始值为#{}.

  2. with-test-tags其中安装一个适当的宏 *tags*.

  3. 一个自定义deftest宏,它扩展为let类似于此的形式(以下是扩展,为了清晰起见略微简化):

    (let [n    (vary-meta '<NAME> update-in [:tags] (fnil into #{}) *tags*)
          form (list* 'clojure.test/deftest n '<BODY>)]
      (eval form))
    
    Run Code Online (Sandbox Code Playgroud)

    <NAME>并且<BODY>是给定制的参数 deftest,通过取消引用语法引用的扩展模板的相应部分插入适当的位置.

因此,自定义的扩展deftest是这样一种let形式,其中,首先,通过用:tags元数据装饰给定符号来准备新测试的名称; 然后clojure.test/deftest构造一个使用这个装饰名称的表单; 最后,后一种形式交给了eval.

这里的关键点是,(eval form)只要它们包含的命名空间是AOT编译的,或者在运行此代码的JVM的生命周期中第一次需要它们,就会对这里的表达式进行求值.这与(println "asdf")顶层完全相同(def asdf (println "asdf")),asdf 只要命名空间是AOT编译或第一次需要,它就会打印出来; 事实上,顶级(println "asdf")行为同样如此.

这可以通过注意到Clojure中的编译只是对所有顶级表单的评估来解释.在(binding [...] (deftest ...), binding顶级表单,但它只返回时deftest ,我们的自定义deftest扩展eval为何时返回的表单 .(另一方面,这种方式require在已经编译的命名空间中执行顶级代码 - 因此,如果你(def t (System/currentTimeMillis))的代码中有值,那么t取决于你需要命名空间的时间而不是编译时的值,就像通过试验AOT编译的代码来确定 - 只是Clojure的工作方式.如果你想在代码中嵌入实际的常量,请使用read-eval.)

实际上,自定义在宏扩展的运行时编译时deftest运行编译器(直通eval).乐趣.

最后,当一个deftest形式把里面with-test-tags的形式,对form(eval 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
Run Code Online (Sandbox Code Playgroud)

只是为了确保正在定义工作测试......

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}
Run Code Online (Sandbox Code Playgroud)