是“透明”的宏可以吗?

发布于 2024-12-01 23:17:15 字数 1650 浏览 0 评论 0原文

我想编写一个 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 这样简单的事情,看起来我自己编写代码会更简单。但我想知道是否有人知道编写宏的技术,该技术“稍微修改”某些子表达式,而不会永远递归。

出于好奇:我考虑了一些其他方法,例如在代码中上下移动时设置一个编译时可绑定的变量,并在我最终看到 < code>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)))

(另外,对于 common-lisp 标签上可能出现的噪音,我深表歉意——我认为即使你的 Clojure 经验很少,你也可以帮助解决更奇怪的宏问题。)

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

两个我 2024-12-08 23:17:15

(这是一种新方法,eval- 和 bounding-free。正如中讨论的
对此答案的评论,使用 eval 是有问题的,因为
它阻止测试关闭它们看起来的词汇环境
定义在 (so (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 宏,将适当的标签注入到
    taget。

  4. 其目的是内部 with-test-tags 形式

    (with-test-tags #{:foo}
      (带有测试标签#{:bar}
        ...))
    

    扩展为(deftest-magic.core/with-test-tags #{:foo :bar} ...)
    (如果确实 deftest-magic.core 是命名空间 with-test-tags
    定义于)。这种形式立即扩展为熟悉的形式
    macrolet 形式,带有 deftestwith-test-tags 符号
    本地绑定到宏,并在内部硬连线正确的标签集


(原始答案更新了一些设计注释,一些
重新措辞和重新格式化等。代码未更改。)

绑定 + 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 会导致
有用的解决方案。基本设计(基于“bounding-able Var”)
idea) 具有三个组件:

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

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

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

    (let [n (vary-meta ' update-in [:tags] (fnil into #{}) *tags*)
          形式(列表*'clojure.test/deftest n')]
      (评估表))
    

    是赋予自定义的参数
    deftest,通过取消引用插入到适当的位置
    语法引用扩展模板的适当部分。

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

这里的关键点是这里的 (eval form) 表达式是
每当它们包含的命名空间是 AOT 编译的或
在运行此的 JVM 的生命周期中第一次需要
代码。这与 a 中的 (println "asdf") 完全相同
顶级 (def asdf (println "asdf")),它将打印 asdf
每当命名空间是 AOT 编译的或第一次需要时
时间
;事实上,顶级 (println "asdf") 的作用类似。

这是通过注意 Clojure 中的编译来解释的:
对所有顶级表格的评估。在(绑定[...](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}

(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}
姐不稀罕 2024-12-08 23:17:15

至少对于 Common Lisp,您只需为隐藏的宏添加别名即可。类似的东西:

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

Clojure 应该有类似的东西。该主题在这里讨论:定义 Clojure 宏的同义词。请注意,定义扩展为“deftest”调用的“deftest2”宏可能不起作用。

我发现这个答案有点晚了,但我还是把它贴在这里,供路人参考。

With Common Lisp at least, you simply alias the shadowed macro. Something like:

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

Clojure should have something similar. The topic is discussed here: define a synonym for a Clojure macro. Be aware that defining a 'deftest2' macro that expands to a 'deftest' call probably won't work.

I see that this answer is a little late, but I'll post it here for passers by.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文