【问题标题】:Use of eval inside macro (global vars unexpectedly resolved)在宏内部使用 eval(全局变量意外解析)
【发布时间】:2021-09-26 17:27:57
【问题描述】:

最近我在宏中遇到了 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 的内容。

【问题讨论】:

    标签: clojure macros


    【解决方案1】:

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

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

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

    而不是

    '(force-eval list-of-things)
    

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

    【讨论】:

    • 核心函数在编译/宏扩展时的可用性确实使它看起来很明显。我想我从来不需要在宏扩展时通过 eval 显式访问全局变量,但没有考虑到我一直在使用核心函数隐式地做这件事...... doh。感谢您的回复。
    • 感谢您的精彩解释!关于“Clojure 中的所有变量都是全局的”的注释。 with-local-vars 呢? clojuredocs.org/clojure.core/with-local-vars
    • @JurajMartinka 当然,但这是一个巨大的边缘案例。没有人使用过该功能,任何知道 var 的人都不是全局的,因此不需要任何说明。它们也不像普通的 var,因为它们不会自动取消引用。
    【解决方案2】:

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

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

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

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

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

    【讨论】:

    • 感谢 Alan - 这非常有帮助,并为我通过观察所理解的内容提供了一些背景,但没有真正理解基础。我想我需要进一步了解 Clojure 的编译过程。
    【解决方案3】:

    而宏中的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))它返回6。因为(1 2 3)+在其前面由apply放在一起然后评估。)

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

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

    【讨论】:

    • 你完全正确 - 这个例子没有任何意义,但这是故意的。我试图将问题集中在宏扩展时全局变量的解析上,而不是宏的有用性。我在开始时确实提到过,我意识到在宏中使用 eval 是不受欢迎的,而且这个例子是人为的。我确实认为宏中有 eval 的(罕见)用例。提供我正在使用的完整代码可能会更清楚,但会掩盖我的问题,这实际上是关于全局变量的解析,而不是宏本身。
    猜你喜欢
    • 2021-05-12
    • 1970-01-01
    • 1970-01-01
    • 2012-12-28
    • 2016-03-07
    • 2017-10-14
    • 2021-10-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多