&Quot;透明&;宏可能吗? [英] Is a "transparent" macrolet possible?

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

问题描述

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

编辑

我刚才做了Post Walk实现,虽然它可以工作,但它不支持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标记和ndash上可能出现的噪音,我很抱歉;我认为您可能能够帮助处理更奇怪的宏代码,即使只有最少的Clojure经验。)

推荐答案

(这是一种新方法,evalbinding是免费的。如中所讨论的 对这个答案的评论,使用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. 我们将macroletdeftest扩展到 clojure.test/deftest附加了适当元数据的表单 测试的名字。这里重要的部分是with-test-tags 将适当的标记集直接注入到 macrolet窗体内的自定义本地deftest;一旦 编译器开始扩展deftest表单、标记集 将被硬连接到代码中。

  3. 如果我们到此为止,在嵌套的 将仅使用传递给 最里面的with-test-tags形式。因此,我们也有with-test-tags 符号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符号 本地绑定到内部具有正确标记集的宏 他们。

    /li>

(原始答案更新了一些关于设计的注释,一些 重新措辞和重新格式化等。代码保持不变。)

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-可用变量) IDEA)有三个组成部分:

  1. 可动态绑定的变量--在编译时绑定 用于装饰deftest表单的一组标记的时间 正在定义测试。默认情况下,我们不添加任何标签,因此其初始 值为#{}

  2. with-test-tags宏,该宏为 *tags*

  3. 展开为类似以下形式的deftest自定义宏 这(下面是扩展,稍微简化了一下 清晰度):

    (let [n    (vary-meta '<NAME> update-in [:tags] (fnil into #{}) *tags*)
          form (list* 'clojure.test/deftest n '<BODY>)]
      (eval form))
    
    <NAME><BODY>是提供给自定义 deftest,通过反引号插入到适当的位置 语法引用的展开模板的适当部分。

因此,定制的扩展是形式,其中, 首先,新测试的名称是通过修饰给定的 带有:tags元数据的符号;然后是clojure.test/deftest表单 使用这个装饰的名字被构造;最后是后一种形式 交给eval

这里的关键点是这里的(eval form)表达式是 只要它们包含的命名空间是AOT编译的或 在运行此命令的JVM的生存期内第一次需要 密码。这与 顶层(def asdf (println "asdf")),将打印asdf 只要命名空间是AOT编译的或第一个 时间;事实上,顶级(println "asdf")的作用类似。

这可以通过注意Clojure中的编译只是 评估所有顶级表单。在[2-68]>中, binding是顶级表单,但仅当deftest时才返回 ,并且我们的自定义deftest扩展为在以下情况下返回的表单 eval。(另一方面,require方式执行顶级 已编译的命名空间中的代码--因此,如果代码中有(def t (System/currentTimeMillis))t的值将 取决于何时需要命名空间,而不是何时需要命名空间 已编译,可以通过测试AOT编译的代码来确定 --这正是Clojure的工作方式。如果您想要Actual,请使用Read-Eval 代码中嵌入的常量。)

实际上,自定义deftest在(通过eval)处运行编译器 宏展开的编译时运行时。好玩。

最后,当deftest表单放在with-test-tags表单中时, 将使用绑定准备(eval form)formwith-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}

这篇关于&amp;Quot;透明&amp;;宏可能吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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