在 clojure 宏中评估符号的正确方法

Proper way of evaluating symbols in clojure macros

我有一个包含一些受信任的 clojure 源代码的文件:

((+ a b) (* a b) (- a b))

我想为列表中的每一项生成一个匿名函数:

(fn [a b] (+ a b))
(fn [a b] (* a b))
(fn [a b] (- a b))

如果我调用下面的 marco

(defmacro create-fn
  [args exprs]
    `(fn ~args ~exprs))

直接使用一些 clojure 代码就可以完美地工作:

user=> (macroexpand-1 '(create-fn [a b] (* a b)))
(clojure.core/fn [a b] (* a b))

但是当我将文件的上下文绑定到本地并尝试映射我的宏时,它将不起作用。在访问第一个生成的函数时,我收到错误消息 "java.lang.RuntimeException: Unable to resolve symbol: a in this context"

(请注意,我不得不在宏中添加一个额外的 eval 来获取符号 e 的值,该符号在 map 使用的匿名函数中使用)

(defmacro create-fn
  [args exprs]
   `(let [e# (eval ~exprs)]
     (fn ~args
       e#)))

(let [exprs (read-string "((+ a b) (* a b) (- a b))")
      fns   (map
             (fn [e] (create-fn [a b] e))
             exprs)]
  (first fns))

非常感谢任何帮助!

让我们看看宏扩展后的整个代码。此代码:

(let [exprs (read-string "((+ a b) (* a b) (- a b))")
      fns   (map
             (fn [e] (create-fn [a b] e))
             exprs)]
  (first fns))

扩展到此,其中 e__900__auto__ 是由 e#:

生成的符号
(let [exprs (read-string "((+ a b) (* a b) (- a b))")
      fns   (map
             (fn [e] (let [e__900__auto__ (eval e)]
                        (fn [a b] e__900__auto__))
             exprs)]
  (first fns))

为什么这不起作用?嗯,原因之一是 ab 甚至不在 (eval e) 的范围内。接下来您可能会想试试这个:

(defmacro create-fn [args exprs] `(fn ~args (eval ~exprs)))

展开后生成的函数如下所示:

(let [exprs (read-string "((+ a b) (* a b) (- a b))")
      fns   (map
             (fn [e] (fn [a b] (eval e)))
             exprs)]
  (first fns))

这看起来不错,但是 it won't work 因为 eval 在空词法环境中求值。换句话说,即使使用此代码,eval 也不会看到 ab

您可以放弃宏,只需手动将代码分解为您可以评估的内容,如下所示:

(map
 (fn [e] (eval (concat '(fn [a b]) (list e))))
 exprs)

或者,您可以将变量 ab 声明为动态变量,然后在计算表达式之前使用 binding 设置它们。

(declare ^:dynamic a ^:dynamic b)

(let [exprs (read-string "((+ a b) (* a b) (- a b))")
      fns   (map
             (fn [e] (fn [a1 b1] (binding [a a1 b b1] (eval e))))
             exprs)]
  (first fns))

如果您不想在您的命名空间中包含 ab,您可以设置另一个命名空间和 evaluate the code there.


我建议的解决方案不使用宏。它们在这里没有用,因为宏在编译时展开,但表达式在运行时读取。如果你真的想在这里使用宏,你需要将 read-string 和文件处理代码移到 defmacro.