【问题标题】:Is a "transparent" macrolet possible?“透明”宏可能吗?
【发布时间】:2011-11-06 15:27:18
【问题描述】:

我想编写一个 Clojure with-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-able var,并在我最终看到 deftest 时使用该 var ,但由于每个宏只返回一个扩展,因此它的绑定不会在下一次调用宏扩展时就位。

编辑

我刚刚完成了 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)))

(另外,对于 common-lisp 标记上可能出现的噪音,我深表歉意——我认为即使您只有很少的 Clojure 经验,也可以帮助处理更奇怪的宏。)

【问题讨论】:

    标签: macros clojure common-lisp


    【解决方案1】:

    (这是一种新方法,eval- 和 binding-free。如在 这个答案的 cmets,使用 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)))))
    

    设计说明:

    1. 我们想要制作基于 macrolet 的设计 问题文本工作。我们关心能够筑巢 with-test-tags 并保留定义测试的可能性 其主体封闭在它们被定义的词汇环境之上 在。

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

    3. 如果我们把它留在那里,测试定义在嵌套的 with-test-tags 只会被传递给 最里面的with-test-tags 表单。因此我们也有with-test-tags macrolet 符号 with-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 定义在)。这种形式立即扩展为熟悉的 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#)))
    

    用法

    (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” 想法)具有三个组成部分:

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

    2. 一个with-test-tags 宏,它安装了一个合适的 *tags*.

    3. 一个自定义的 deftest 宏,它扩展为类似于 let 的形式 this(以下是展开,稍微简化一下 清晰度):

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

      &lt;NAME&gt;&lt;BODY&gt; 是自定义的参数 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 表单时, (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}
    

    【讨论】:

    • 非常有趣。在编译时很难想象eval 的有效用例,但是这个是有道理的。几个问题:(1) 有没有办法用macrolet 做到这一点,这样用户只需要引用with-test-tags,而不会创建冲突的deftest 引用? (2) 这意味着deftest 形式不能是词法闭包,如(let [x 1] (deftest ...)),这有点不幸。你觉得有什么办法吗?
    • 澄清以上内容:我尝试将您的解决方案移植到使用macrolet,但也必须使用 三个 不同级别的引用和不同的执行时间对我来说很重要。
    • 当然,可以用macrolet来完成。 Here is the Gist of it. 稍后我会在这里发布,但目前,Gist 包含一个最小的工作项目,其中包含基于 macrolet(来自 tools.macro 的版本)和一些测试的 with-test-tags 版本。用法不变,除了客户端代码使用标准deftest。值得庆幸的是,自定义deftest 的定义可以使用适当的~' 直接注入macrolet。 :-)
    • 至于(2),同意这是不幸的。我会再考虑一下;我看到的一种有希望的方法似乎有一个明显的问题,但无论如何我都会尝试一下......
    • 好的,我有一个 eval- 和 binding-free 解决方案,它通过了为以前版本编写的所有测试以及一个新测试,以验证测试可以是闭包并在同时。 The Gist is here.一会儿我也发到这里,还有设计的小总结。
    【解决方案2】:

    至少使用 Common Lisp,您只需为阴影宏设置别名。比如:

    (setf (macro-function 'deftest2) (macro-function 'deftest))
    (defmacro with-test-tags (etc...)
      `(macrolet ((deftest (etc...)
                     ``(deftest2 ...
    

    Clojure 应该有类似的东西。此处讨论该主题:define a synonym for a Clojure macro。请注意,定义扩展为“deftest”调用的“deftest2”宏可能不起作用。

    我看到这个答案有点晚了,但我会在这里发布给路人。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-09-10
      • 1970-01-01
      • 1970-01-01
      • 2012-02-01
      • 2014-01-08
      • 2013-11-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多