【问题标题】:Evaluation of arguments in function called by macro评估宏调用的函数中的参数
【发布时间】:2019-06-21 00:59:51
【问题描述】:

在明确告知之前,宏不会评估它们的参数,但是函数会这样做。在以下代码中:

(defmacro foo [xs]
  (println xs (type xs)) ;; unquoted list
  (blah xs))

(defn blah [xs] ;; xs is unquoted list, yet not evaluated
  (println xs)
  xs)

(foo (+ 1 2 3))

似乎blah 没有评估xs,因为我们仍然有整个列表:(+ 1 2 3) 在 blah 的正文中绑定到 xs

我基本上只是记住了宏中的辅助函数和它们对参数的评估之间的这种交互,但老实说,这违背了我的直觉(xs 将在进入正文之前被评估,因为函数参数总是被评估)。

我的想法基本上是:“好的,在这个宏主体中,我有 xs 作为未评估列表,但如果我从宏中调用带有 xs 的函数,它应该评估该列表”。

很明显,我对事情的运作方式有一个令人尴尬的根本性误解。我的解释中缺少什么?评估实际上是如何进行的?

编辑


我对此进行了更多思考,在我看来,将宏参数视为“隐式引用”可能会解决我的一些困惑。

我想我只是混淆了各种术语,但鉴于引用的形式与未评估的形式同义,并且鉴于宏参数未评估,它们被隐式引用。

所以在我上面的例子中,说xs 没有被引用有点误导。例如这个宏:

(defmacro bluh [xs]
  `(+ 1 2 ~xs))

与下面的宏基本相同(不包括符号上的命名空间)。在对list 的调用中解析xs 会返回一个未评估(引用?)的列表。

(defmacro bleh [xs]
  (list '+ '1 '2 xs)) ;; xs resolves to a quoted list (or effectively quoted)

调用 bleh(或 bluh)等同于说:

(list '+ '1 '2 '(+ 1 2 3)) 
;; => (+ 1 2 (+ 1 2 3))

如果xs 没有解析为引用列表,那么我们最终会得到:

(list '+ '1 '2 (+ 1 2 3)) 
;; => (+ 1 2 6)

因此,简而言之,宏参数被引用

我认为我的部分困惑来自于将语法引用表单视为带有插槽的模板,例如(+ 1 2 ~xs) 我会在心理上扩展为 (+ 1 2 (+ 1 2 3)),并且看到 (+ 1 2 3) 在该扩展中没有被引用,我发现使用 xs 的函数调用(在上面的第一个示例中 blah)不会立即进行评估,这很令人困惑到6

模板隐喻很有帮助,但如果我将其视为 (list '+ '1 '2 xs) 的快捷方式很明显 xs 必须是带引号的列表,否则扩展将包括 6 而不是整个列表。

我不知道为什么我会觉得这很令人困惑...我做对了还是我完全走错了路?

【问题讨论】:

  • xs in blah 应该是 6,即使您通过 foo 调用时也是如此
  • @Sylwester 哎呀,我的意思是通过 foo 调用,而不是直接调用...修复示例。不,xs 通过 foo 调用时实际上不是 6,它仍然是列表 (+ 1 2 3)
  • 但是你没有返回(blah (+ 1 2 3)),而是使用xs作为列表调用(blah xs)的结果..因此来自blah的扩展是(+ 1 2 3)。基本上你是不断折叠blah
  • 我看到你正在考虑扼杀你的问题。请不要。它有两个赞成票,包括我的。对于Svante's answer,如果我是你,我会接受,这是对一个困难话题的有价值的讨论。我喜欢你隐式引用宏参数的表述。

标签: clojure macros lisp


【解决方案1】:

[这个答案试图解释为什么不评估其参数的宏和函数是不同的东西。我相信这适用于 Clojure 中的宏,但我不是 Clojure 专家。也太长了,抱歉。]

我认为您对 Lisp 所谓的宏和现代 Lisp 没有但过去称为 FEXPR 的结构感到困惑。

您可能想要两种有趣的、不同的东西:

  • 调用时不立即评估其参数的函数;
  • 语法转换器,在 Lisp 中称为

我会按顺序处理的。

不立即评估其参数的函数

在传统的 Lisp 中,(f x y ...) 之类的形式(其中 f 是一个函数)将:

  1. 确定f 是一个函数,而不是什么特殊的东西;
  2. 获取与f 对应的函数,并以语言指定的某种顺序(可能是“未指定顺序”)评估xy 和其余参数;
  3. 调用 f 并获取参数评估结果。

最初需要步骤 (1),因为 f 可能是一个特殊的东西(比如 ifquote),并且可能在 (1) 中也检索到函数定义:所有这一切,以及事情在 (2) 中发生的顺序都是语言需要定义的(或者,在 Scheme 的情况下,明确地未定义)。

这种排序,特别是 (2) 和 (3) 的排序被称为 applicative ordereager evaluation(我将在下面将其称为 applicative order )。

但还有其他可能性。其中之一是不评估参数:调用函数,并且仅当参数的值是需要时才评估它们。有两种方法可以做到这一点。

第一种方法是定义语言,以便 所有 函数以这种方式工作。这称为 惰性求值正常顺序 求值(下面我将其称为正常顺序)。在正常的顺序语言中,函数参数通过魔术在需要的时候被评估。如果他们永远不需要,那么他们可能永远不会被评估。所以在这样的语言中(我在这里发明了函数定义的语法,以免提交 CL 或 Clojure 或其他任何东西):

(def foo (x y z)
  (if x y z))

只有yz 之一将在对foo 的调用中进行评估。

在正常顺序的语言中,您不需要明确关心何时评估事物:该语言确保在需要时评估它们。

正常顺序语言似乎是一个明显的胜利,但我认为它们往往很难使用。有两个问题,一个明显,一个不太明显:

  • 副作用发生的顺序比应用顺序语言更难预测,而且可能根本不会发生,因此习惯于以命令式风格编写的人(大多数人)发现它们很难处理;李>
  • 即使是无副作用的代码,其行为也可能与应用顺序语言不同。

副作用问题可以被视为一个非问题:我们都知道有副作用的代码很糟糕,对吧,那么谁在乎呢?但即使没有副作用,情况也有所不同。例如,这里是一个正常顺序语言中 Y 组合子的定义(这是一种非常简朴的、正常顺序的 Scheme 子集):

(define Y
  ((λ (y)
     (λ (f)
       (f ((y y) f))))
   (λ (y)
     (λ (f)
       (f ((y y) f))))))

如果您尝试在应用顺序语言中使用此版本的 Y(如普通 Scheme),它将永远循环。以下是 Y 的适用订单版本:

(define Y
  ((λ (y)
     (λ (f)
       (f (λ (x)
            (((y y) f) x)))))
   (λ (y)
     (λ (f)
       (f (λ (x)
            (((y y) f) x)))))))

你可以看到它是一样的,但那里有额外的 λs 基本上“延迟”评估以阻止它循环。

正常顺序评估的第二种方法是使用一种语言,该语言主要是应用顺序,但其中有一些特殊的机制来定义不评估其参数的函数。在这种情况下,通常需要一些特殊的机制来在函数体中说“现在我想要这个参数的值”。历史上这样的东西被称为FEXPRs,它们存在于一些非常古老的 Lisp 实现中:Lisp 1.5 有它们,我认为 MACLISP 和 InterLisp 也有它们。

在具有 FEXPR 的应用顺序语言中,您需要以某种方式能够说“现在我想评估这件事”,我认为这是正在面临的问题:该事物在什么时候决定评估论据?好吧,在一个纯动态范围的非常古老的 Lisp 中,有一个令人作呕的 hack 来做到这一点:在定义 FEXPR 时,你可以传入 参数的来源,然后,当你想要它的值时,你只需拨打EVAL就可以了。这只是一个糟糕的实现,因为这意味着 FEXPR 永远无法真正正确编译,并且您必须使用动态范围,因此变量永远无法真正被编译掉。但这就是一些(全部?)早期实现的方式。

但是 FEXPR 的这种实现允许一个惊人的 hack:如果你有一个 FEXPR,它已经给出了它的参数的来源,并且你知道这就是 FEXPR 的工作方式,那么,它可以在调用 @ 之前操纵那个来源987654341@ 就可以了:它可以调用EVAL 代替从源派生的东西。而且,事实上,它所获得的“来源”甚至根本不需要是严格合法的 Lisp:它可以是 FEXPR 知道如何操纵的东西。这意味着您可以突然间以非常通用的方式扩展语言的语法。但这样做的代价是您无法编译任何这些:您构造的语法必须在运行时进行解释,并且每次调用 FEXPR 时都会发生转换。

语法转换器:宏

因此,除了使用 FEXPR,您还可以做其他事情:您可以更改评估的工作方式,以便在发生其他任何事情之前,有一个阶段代码会被遍历并可能转换为其他代码(也许更简单的代码)。而且这只需要发生一次:一旦代码被转换,那么结果可以被隐藏在某个地方,并且不需要再次发生转换。所以这个过程现在看起来像这样:

  1. 读入代码并从中构建结构;
  2. 这个初始结构可能被转换成其他结构;
  3. (结果结构可能已编译);
  4. 可能会多次评估生成的结构或编译结果。

所以现在评估过程被分成几个“次”,它们不重叠(或对于特定定义不重叠):

  1. 读取时间是构建初始结构的时间;
  2. 宏膨胀时间是变换的时间;
  3. 编译时间(可能不会发生)是编译结果的时间;
  4. 评估时间是评估的时间。

好吧,所有语言的编译器都可能会做这样的事情:在将源代码实际转换为机器可以理解的东西之前,它们会进行各种源代码到源代码的转换。但是这些东西在编译器的内部,并且在源代码的某些表示上运行,这些表示对该编译器来说是特殊的,而不是由语言定义的。

Lisp 向用户开放这个过程。该语言有两个特性使这成为可能:

  • 从源代码读取后创建的结构由语言定义,并且该语言具有丰富的工具集来操作该结构;
  • 所创建的结构相当“低承诺”或简朴 - 在许多情况下,它不会让您特别容易做出任何解释。

作为第二点的一个例子,考虑(in "my.file"):这是一个名为in的函数的函数调用,对吧?好吧,可能是:(with-open-file (in "my.file") ...) 几乎可以肯定不是函数调用,而是将in 绑定到文件句柄。

由于语言的这两个特性(实际上还有一些我不会深入探讨),Lisp 可以做一件很棒的事情:它可以让语言的用户编写这些语法转换函数 -- 宏 -- 在便携式 Lisp 中。

剩下的唯一事情就是决定如何在源代码中标注这些宏。答案和函数是一样的:当你定义一些宏m时,你就像(m ...)一样使用它(一些Lisps支持更通用的东西,比如CL's symbol macros。在宏扩展时——在程序被读取但在(编译和)运行之前 - 系统遍历程序的结构以查找具有宏定义的事物:当它找到它们时,它会调用与宏对应的函数,并使用其参数指定的源代码,并且宏返回一些其他的源代码块,依次遍历,直到没有剩下的宏(是的,宏可以扩展到涉及其他宏的代码,甚至涉及到自己的代码)。一旦这个过程完成,那么生成的代码可以(编译和)运行。

因此,尽管宏在代码中看起来像函数调用,但它们只是不评估其参数的函数,就像 FEXPR 那样:相反,它们是需要一些 Lisp 源代码的函数并返回另一段 Lisp 源代码:它们是语法转换器,或对源代码(语法)进行操作并返回其他源代码的函数。宏在评估时间之前的宏扩展时间运行(见上文)。

所以,实际上宏函数,用 Lisp 编写,它们调用的函数完全按照惯例评估它们的参数:一切都很普通。但是宏的参数是 programs (或表示为某种 Lisp 对象的程序的语法),它们的结果是其他程序的(的语法)。如果您愿意,宏是元级别的函数。因此,如果一个计算(部分)程序的函数是一个宏:这些程序可能稍后自己运行(也许更晚,也许永远不会),此时评估规则将应用于它们。但是此时宏被称为它所处理的只是程序的语法,而不是评估该语法的一部分。

所以,我认为您的心智模型是宏类似于 FEXPR,在这种情况下,“如何评估参数”问题是显而易见的问题。但它们不是:它们是计算程序的函数,并且在它们计算的程序运行之前就可以正常运行。

抱歉,这个答案太冗长了。


FEXPR 发生了什么?

FEXPR 总是很成问题。例如(apply f ...) 应该做什么?由于f 可能是 FEXPR,但这通常要到运行时才能知道,所以很难知道正确的做法是什么。

所以我认为发生了两件事:

  • 在人们真正想要正常顺序语言的情况下,他们实施了这些语言,并且对于这些语言,评估规则处理了 FEXPR 试图处理的问题;
  • 在应用顺序语言中,如果您不想评估某些参数,您现在可以通过明确说明使用 delay 等构造来构造“承诺”和 force 来强制评估承诺 - 因为语言的语义得到改进,可以完全用该语言实现 Promise(CL 没有 Promise,但实现它们本质上是微不足道的)。

我描述的历史正确吗?

我不知道:我认为可能是,但也可能是理性的重构。我当然,在非常古老的 Lisps 的非常古老的程序中,已经看到 FEXPR 以我描述的方式使用。我认为 Kent Pitman 的论文,Special Forms in Lisp 可能有一些历史:我过去读过它,但直到现在才忘记它。

【讨论】:

    【解决方案2】:

    宏定义是转换代码的函数的定义。宏函数的输入是宏调用中的forms。宏函数的返回值将被视为插入宏形式的代码。 Clojure 代码由 Clojure 数据结构(主要是列表、向量和映射)组成。

    在您的foo 宏中,您定义宏函数以返回blah 对您的代码 所做的任何事情。由于blah (几乎)是identity 函数,它只返回它的输入。

    您的情况如下:

    • 字符串"(foo (+ 1 2 3))"读取,生成一个包含两个符号和三个整数的嵌套列表:(foo (+ 1 2 3))
    • foo 符号被解析为宏 foo
    • 调用宏函数foo,其参数xs绑定到列表(+ 1 2 3)
    • 宏函数(打印然后)使用列表调用函数blah
    • blah(打印然后)返回该列表。
    • 宏函数返回列表。
    • 宏因此“扩展”为(+ 1 2 3)
    • 符号+ 解析为加法函数。
    • 使用三个参数调用加法函数。
    • 加法函数返回它们的总和。

    如果您希望将宏 foo扩展 以调用 blah,则需要返回这样的表单。 Clojure 提供了一种使用反引号的模板方便语法,因此您不必使用list 等来构建代码:

    (defmacro foo [xs]
      `(blah ~xs))
    

    就像:

    (defmacro foo [xs]
      (list 'blah xs))
    

    【讨论】:

    • 我认为处理“转换代码的函数”的一种好方法是查看 Racket 中的宏。在 Racket 中,宏 以列表和 c 的方式获得代码的表示形式,就像它在 CL 和(我认为)Clojure 中所做的那样:它获得一个语法对象,然后需要将其转换为另一个语法对象,它是宏的返回值。语法对象内部是您期望的源的表示形式,但它们具有与它们相关的其他信息,这些信息涉及卫生等。
    • @Svante 感谢您的回复。我知道从我的问题的性质来看可能看起来不是这样,但我理解你上面所说的一切(除了一部分)。我感到困惑的部分是为什么 blah 不评估列表。那时我们已经离开了宏域并处于函数调用中,并且函数会评估参数。我想一旦我们处理一个列表数据结构,它就不再评估为调用而是对自身进行评估?但如果是这种情况,那么在宏扩展代码中,它在运行时也不会评估为 6,而是评估为自身 - 所以显然这不是真的。
    • @svante 我完全理解宏不会评估它们的形式,它们会获取数据并返回数据,然后在运行时对其进行评估。在这种情况下,数据是一个表示函数调用的列表,但是当您从宏中使用它调用函数时,从宏返回时在运行时计算为 6 的相同列表不会计算为 6。从宏返回时列表的评估规则在运行时与编译时作为参数传递给函数时有什么不同,否则通常会评估列表?
    • @Solaxun: 不,你还没有 'left macroland':宏调用的任何函数或它通常运行的任何代码都发生在宏扩展时时间,正好在评估时间之前。
    • @Solaxun:不要删除它(好吧,我宁愿你没有:显然选择是你的)。我认为这不是一个坏问题,我打算尝试写另一个答案以防万一。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-08-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-21
    • 1970-01-01
    相关资源
    最近更新 更多