在宏中使用 eval(全局变量意外解析)

Use of eval inside macro (global vars unexpectedly resolved)

最近我发现在宏中使用 eval,我知道这有点失礼,但我们暂时忽略它。让我感到惊讶的是 eval 能够在宏扩展时解析全局变量。下面是一个人为的例子,只是为了说明我所指的情况:

(def list-of-things (range 10))

(defmacro force-eval [args]
  (apply + (eval args)))

(macroexpand-1 '(force-eval list-of-things))

; => 45

我本以为 args 解析为 force-eval 中的符号 list-of-things,然后 list-of-things 被求值,由于未绑定而导致错误:

"unable to resolve symbol list-of-things in this context"

然而,list-of-things 被解析为 (range 10) 并且没有抛出任何错误 - 宏扩展成功。

将此与尝试执行相同的宏扩展进行对比,但在本地绑定上下文中:

(defmacro force-eval [args]
  (apply + (eval args)))

(let [list-of-things (range 10)]
  (macroexpand-1 '(force-eval list-of-things)))

; => Unable to resolve symbol: list-of-thingss in this context

请注意,在上面的示例中,我假设 list-of-things 之前未绑定,例如一个新的 REPL。最后一个例子说明了为什么这很重要:

(defmacro force-eval [args]
  (apply + (eval args)))

(def list-of-things (range 10 20))

(let [list-of-thing (range 10)]
  (macroexpand-1 '(force-eval list-of-things)))

; => 145

上面的例子表明局部变量被忽略了,这是 eval 的预期行为,但是当您期望全局变量在宏扩展时也不可用时会有点混乱。

我似乎对宏展开时究竟可以使用什么有误解。我以前认为基本上任何绑定,无论是全局的还是本地的,在运行时之前都是不可用的。显然这是一个错误的假设。 我的困惑的答案仅仅是全局变量在宏扩展时可用吗?还是我在这里遗漏了一些细微差别?

注意this related post详细描述了类似的问题,但重点更多地放在如何避免不当使用eval上。我主要想了解为什么 eval 在第一个示例中起作用,以及扩展在宏扩展时 eval 可用的内容。

当然,vars 必须在编译时可见。那就是存储 first+ 等函数的地方。没有他们,你什么都做不了。

但请记住,您必须确保正确引用它们。在 repl 中,*ns* 将被绑定,因此对符号的引用将在当前命名空间中查找。如果你是 运行 通过 -main 而不是 repl 的程序,*ns* 将不会被绑定,并且只会找到适当限定的变量。您可以使用

确保您正确地限定了它们
`(force-eval list-of-things)

而不是

'(force-eval list-of-things)

注意我不区分全局变量和非全局变量。 Clojure 中的所有变量都是全局的。本地绑定不称为变量。它们被称为局部变量、绑定、变量或这些词的某种组合。

eval在宏中是没有意义的。 因为:

  1. 一个宏已经隐式包含一个 eval 在最后一步。

    如果您使用 macroexpand-1,您可以看到代码是如何在宏 之前 调用宏中的隐式 eval 之前操作的。

    宏中的 eval 是一种反模式,它可能表明您应该使用函数而不是宏 - 在您的示例中正是这种情况。

  2. 因此您的目标是动态地(在 运行 时间内)在宏中唤起某事。这只能通过在宏调用上应用 eval 来完成,或者您应该使用函数。

(defmacro force-eval [args]
  (apply + (eval args)))

;; What you actually mean is:
(defn force-eval [args]
  (apply + args))
;; because a function in lisp evaluates its arguments 
;; - before applying the function body.
;; That means: args in the function body is exactly
;; `(eval args)`!

(def list-of-things (range 10))
(let [lit-of-things (range 10 13)]
  (force-eval list-of-things))
;; => 45

;; so this is exactly the behavior you wanted!

重点是,您的构造是宏的“坏”示例。 因为 apply 是一个特殊的函数,它允许你 动态重新排列函数调用结构 - 所以它有 它里面有一些宏的魔力 - 但在 运行 时间内。 使用 apply 在某些情况下,当您只引用一些输入参数时,您可以进行相当多的元编程。 (试试(force-eval '(1 2 3))它returns6。因为(1 2 3)+在它前面被apply放在一起然后求值。)

第二点 - 我正在考虑this answer I once gave and Common Lisp 中的一个动态宏调用问题。

简而言之:当您必须在宏中控制两个级别的评估时(通常当您希望宏在 运行 时间内将一些代码注入某些代码时),您也需要使用 eval 在调用宏时评估宏调用中的那些部分,然后应在宏中处理这些部分。

Clojure 采用增量编译模型设计。这没有很好的记录。

在C和其他传统语言中,源代码必须经过编译,然后与预编译的库链接,才能执行最终结果。一旦开始执行,在程序终止之前不会对代码进行任何更改,此时可以编译、链接然后执行新的源代码。 Java 通常以这种方式使用,就像 C.

使用 Clojure REPL,您可以在实时执行环境中从零源代码开始。您可以调用现有函数,如 (+ 2 3),或者您可以动态定义新函数和变量(全局和局部),并重新定义现有函数。这是唯一可能的,因为核心 Clojure 已经可用(即 clojure.core/+ 等已经“安装”),因此您可以组合这些函数来定义您自己的新函数。

Clojure“编译器”的工作方式就像一个巨大的 REPL 会话。它一次从您的源代码文件中读取和评估表单,逐步将它们添加到全局环境中。事实上,编译和执行源代码的结果与将每个完整源代码文件粘贴到 REPL 中(以正确的依赖顺序)时所发生的结果是相同的,这是一种设计goal/requirement。

的确,在 Clojure 中执行代码最简单的心智模型是假装它是一个解释器而不是传统的编译器。