[这个答案试图解释为什么不评估其参数的宏和函数是不同的东西。我相信这适用于 Clojure 中的宏,但我不是 Clojure 专家。也太长了,抱歉。]
我认为您对 Lisp 所谓的宏和现代 Lisp 没有但过去称为 FEXPR 的结构感到困惑。
您可能想要两种有趣的、不同的东西:
- 调用时不立即评估其参数的函数;
-
语法转换器,在 Lisp 中称为 宏。
我会按顺序处理的。
不立即评估其参数的函数
在传统的 Lisp 中,(f x y ...) 之类的形式(其中 f 是一个函数)将:
- 确定
f 是一个函数,而不是什么特殊的东西;
- 获取与
f 对应的函数,并以语言指定的某种顺序(可能是“未指定顺序”)评估x、y 和其余参数;
- 调用
f 并获取参数评估结果。
最初需要步骤 (1),因为 f 可能是一个特殊的东西(比如 if 或 quote),并且可能在 (1) 中也检索到函数定义:所有这一切,以及事情在 (2) 中发生的顺序都是语言需要定义的(或者,在 Scheme 的情况下,明确地未定义)。
这种排序,特别是 (2) 和 (3) 的排序被称为 applicative order 或 eager evaluation(我将在下面将其称为 applicative order )。
但还有其他可能性。其中之一是不评估参数:调用函数,并且仅当参数的值是需要时才评估它们。有两种方法可以做到这一点。
第一种方法是定义语言,以便 所有 函数以这种方式工作。这称为 惰性求值 或 正常顺序 求值(下面我将其称为正常顺序)。在正常的顺序语言中,函数参数通过魔术在需要的时候被评估。如果他们永远不需要,那么他们可能永远不会被评估。所以在这样的语言中(我在这里发明了函数定义的语法,以免提交 CL 或 Clojure 或其他任何东西):
(def foo (x y z)
(if x y z))
只有y 或z 之一将在对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,您还可以做其他事情:您可以更改评估的工作方式,以便在发生其他任何事情之前,有一个阶段代码会被遍历并可能转换为其他代码(也许更简单的代码)。而且这只需要发生一次:一旦代码被转换,那么结果可以被隐藏在某个地方,并且不需要再次发生转换。所以这个过程现在看起来像这样:
- 读入代码并从中构建结构;
- 这个初始结构可能被转换成其他结构;
- (结果结构可能已编译);
- 可能会多次评估生成的结构或编译结果。
所以现在评估过程被分成几个“次”,它们不重叠(或对于特定定义不重叠):
-
读取时间是构建初始结构的时间;
-
宏膨胀时间是变换的时间;
-
编译时间(可能不会发生)是编译结果的时间;
-
评估时间是评估的时间。
好吧,所有语言的编译器都可能会做这样的事情:在将源代码实际转换为机器可以理解的东西之前,它们会进行各种源代码到源代码的转换。但是这些东西在编译器的内部,并且在源代码的某些表示上运行,这些表示对该编译器来说是特殊的,而不是由语言定义的。
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 可能有一些历史:我过去读过它,但直到现在才忘记它。