为什么在 Clojure 中将元数据添加到函数定义的工作方式与其他形式不同?

Why does adding metadata to a function definition work differently than other forms, in Clojure?

为什么会失败:

(eval (with-meta '(fn [] 0) {:stack (gensym "overflow")}))
; Syntax error compiling at (REPL:1:1).
; Unable to resolve symbol: overflow210 in this context

当以下 none 失败时?

(eval (with-meta '(do [] 0) {:stack (gensym "overflow")}))
; 0
(eval (with-meta '(let [] 0) {:stack (gensym "overflow")}))
; 0
(eval (with-meta '(if true 0 1) {:stack (gensym "overflow")}))
; 0
(eval (with-meta '(println "hello") {:stack (gensym "overflow")}))
; hello
; nil

上面的例子是我试图找到一个最小的、可重现的例子。我在处理宏时 运行 遇到了这个问题,这里有一个简化的例子:

(defmacro my-macro []
  (with-meta '(fn [] 0) {:stack (gensym "overflow")}))

(my-macro)
; Syntax error compiling at (REPL:1:1).
; Unable to resolve symbol: overflow156 in this context

testing Clojure macros 上尝试遵循此 post 中解释的模型时。

问题是您对 gensym 的使用,而不是元数据。观察:

(ns tst.demo.core
  (:use demo.core tupelo.core tupelo.test))

(dotest
  (binding [*print-meta* true]
    (let [
          fn-code-plain  '(fn [] 42)
          fn-code-meta   (with-meta '(fn [] 42) {:alpha true})

          fn-code-sym-meta   (with-meta '(fn [] 42) {:alpha (gensym "dummy")})
          ]
      (prn fn-code-plain)
      (prn fn-code-meta)
\
      (prn (eval fn-code-plain))
      (prn (eval fn-code-meta))
      (newline)
      (prn fn-code-sym-meta)

      (throws? (eval fn-code-sym-meta))

      )
    ))

结果:

^{:line 7, :column 27} (fn [] 42)
^{:alpha true}         (fn [] 42)

#object[user$eval19866$fn__19867 0xac891b5 "user$eval19866$fn__19867@ac891b5"]
^{:alpha true} #object[user$eval19870$fn__19871 0x2d1ab7e0 "user$eval19870$fn__19871@2d1ab7e0"]

^{:alpha dummy19865} (fn [] 42)

问题是 eval 看到符号 dummy19865 并尝试解决它。它是由 gensym 创建的事实无关紧要。关键字没问题:

  fn-code-kw-meta   (with-meta '(fn [] 42) {:alpha :dummy-42})
  <snip>
  (prn fn-code-kw-meta)
  (prn (eval fn-code-kw-meta))

生产:

^{:alpha :dummy-42} (fn [] 42)
^{:alpha :dummy-42} #object[user$eval19953$fn__19954 0x13253ac7 "user$eval19953$fn__19954@13253ac7"]

或定义的符号:

(def mysym "Forty-Two!")
<snip>

  fn-code-mysym-meta   (with-meta '(fn [] 42) {:alpha mysym})
  <snip>

(prn fn-code-mysym-meta)
(prn (eval fn-code-mysym-meta))

结果:

^{:alpha "Forty-Two!"} (fn [] 42)
^{:alpha "Forty-Two!"} #object[user$eval20082$fn__20083 0x24a63de5 "user$eval20082$fn__20083@24a63de5"]

总结:

您已经证明 eval 仅尝试对 fn 形式的元数据进行符号解析,而不是 dolet、[=25 等其他特殊形式=],或使用预先存在的函数,例如 println。如果您想进一步探索,您可能应该查询 Clojure 电子邮件列表:

clojure@googlegroups.com 

以上代码基于this template project.

好问题!这很有趣。

首先尝试将元数据映射设置为有效的内容并在每个示例中检索它是有帮助的:

(meta (eval (with-meta '(fn [] 0) {:ten 10})))
;;=> {:ten 10}
(meta (eval (with-meta '(do [] 0) {:ten 10})))
;;=> nil
(meta (eval (with-meta '(let [] 0) {:ten 10})))
;;=> nil
(meta (eval (with-meta '(if true 0 1) {:ten 10})))
;;=> nil
(meta (eval (with-meta '(println "hello") {:ten 10})))
;; printed: hello
;;=> nil

希望这能让您对这里发生的事情有所了解:元数据不会作为非 fn 表单值的一部分返回,因此不会对其进行评估。我们可以用另一个使用元数据的值来检验这个假设,比如向量:

(meta (eval (with-meta '[1] {:ten 10})))
;;=> {:ten 10}

但是 gensym:

(eval (with-meta '[1] {:stack (gensym "overflow")}))
;;=> Syntax error compiling at (tmp:localhost:35479(clj)*:25:7).
;;=> Unable to resolve symbol: overflow6261 in this context

您可以看到 where this is emitted in the Clojure compiler,搜索 new MetaExpr 将向您显示发出元数据评估的其他位置(我可以看到向量、映射、集合、函数和 reify).

tl,dr: Clojure 计算函数形式的元数据,因为它将计算的元数据附加到生成的函数。 Clojure 语法中支持的数据文字也是如此。表单上的任何其他元数据都会被编译去除,因此不会对其进行评估,因此不会导致符号解析错误。