在明确告知之前,宏不会评估它们的参数,但是函数会这样做。在以下代码中:
(defmacro foo [xs] (println xs(type xs));;不带引号的清单 (blah xs))
(定义…
[这个答案试图解释为什么不评估其参数的宏和函数是不同的东西。我相信这适用于Clojure中的宏,但我不是Clojure的专家。它也太长了,抱歉。]
我认为你在Lisp调用的宏和现代Lisps没有的构造之间感到困惑,而这种构造曾经被称为FEXPRs。
你可能想要两件有趣的,不同的东西:
我会按顺序处理它们。
在传统的Lisp中,形式如 (f x y ...) ,哪里 f 是一个功能,将:
(f x y ...)
f
x
y
最初需要步骤(1),因为 f 可能是一件特别的事情(比如说 if , 要么 quote ),也可能是在(1)中检索函数定义:所有这些,以及(2)中发生的事情的顺序是语言需要定义的东西(或者,在方案说,明确未定义)。
if
quote
这种排序,特别是(2)&的排序。 (3)被称为 适用的命令 要么 急切的评价 (我将在下面称之为申请顺序)。
但还有其他可能性。其中之一是不对参数进行求值:调用函数,并且仅在参数值为时才调用 需要 他们评估了吗?有两种方法可以做到这一点。
第一种方法是定义语言,以便 所有 功能以这种方式工作。这就是所谓的 懒惰的评价 要么 正常的顺序 评价(我将其称为下面的正常订单)。在正常的顺序中,语言函数参数通过魔术在需要时进行评估。如果他们永远不需要,那么他们可能永远不会被评估。所以在这种语言中(我在这里发明函数定义的语法,以便不提交CL或Clojure或其他任何东西):
(def foo (x y z) (if x y z))
只有一个 y 要么 z 将在电话会议中进行评估 foo 。
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)))))))
你可以看到它有点相同,但是那里有额外的whichs基本上“懒散”评估以阻止它循环。
正常秩序评估的第二种方法是使用一种主要是应用顺序的语言,但其中有一些特殊的用于定义不评估其参数的函数的机制。在这种情况下,通常需要一些特殊的机制来在函数体中说“现在我想要这个参数的值”。历史上这样的事情被称为 FEXPRs ,它们存在于一些非常古老的Lisp实现中:Lisp 1.5有它们,我认为它们都是MACLISP& InterLisp也有它们。
在使用FEXPRs的应用顺序语言中,你需要以某种方式能够说'现在我想评估这个东西',并且我认为这是正在遇到的问题:在什么时候该事情决定评估参数?好吧,在一个非常古老的Lisp中,这是一个纯粹的动态范围,有一个恶心的黑客来做到这一点:在定义FEXPR时你可以传入 论证的来源 然后,当你想要它的价值时,你只需要打电话 EVAL 在上面。这只是一个糟糕的实现,因为它意味着FEXPR永远不会真正被正确编译,并且你必须使用动态范围,因此变量永远不会真正被编译掉。但这是一些(所有?)早期实现的方式。
EVAL
但是这个FEXPR的实现允许一个惊人的黑客:如果你有一个FEXPR已被赋予其参数的来源,并且你知道这就是FEXPRs的工作方式,那么,它可以在调用之前操纵该源 EVAL 在它上面:它可以打电话 EVAL 取而代之的是源自某个东西。而且,事实上,它所获得的“来源”甚至根本不需要严格合法的Lisp:它可以是FEXPR知道如何操纵以制造某些东西的东西。这意味着您可以突然以非常一般的方式扩展语言的语法。但是能够做到这一点的代价是你不能编译任何这些:你构造的语法必须在运行时解释,并且每次调用FEXPR时都会发生转换。
因此,除了使用FEXPR之外,您还可以执行其他操作:您可以更改评估的工作方式,以便在其他任何事情发生之前,有一个阶段,在此阶段代码被遍历并可能转换为其他代码(更简单的代码) , 也许)。而且这种需要只发生一次:一旦代码被转换,那么结果就可能被隐藏在某个地方,并且转换不需要再次发生。所以这个过程现在看起来像这样:
所以现在评估过程被分成几个“时间”,它们不重叠(或者对于特定的定义不重叠):
好吧,所有语言的编译器可能会做这样的事情:在实际将源代码转换为机器理解的东西之前,他们将进行各种源到源的转换。但是这些东西都在编译器的内部,并且正在对源的某些表示进行操作,这对于该编译器来说是特殊的并且不是由该语言定义的。
Lisp向用户开放此过程。该语言有两个功能,使这成为可能:
作为第二点的一个例子,考虑一下 (in "my.file") :这是一个函数调用函数调用 in , 对?也许: (with-open-file (in "my.file") ...) 几乎肯定不是函数调用,而是绑定 in 到文件句柄。
(in "my.file")
in
(with-open-file (in "my.file") ...)
由于语言的这两个特性(事实上其他一些我不会进入),Lisp可以做一件很棒的事情:它可以让语言的用户编写这些语法转换函数 - 宏 - 在便携式Lisp中 。
唯一剩下的就是决定如何在源代码中标注这些宏。答案与函数的作用相同:当你定义一些宏时 m 你就像使用它一样 (m ...) (有些Lisps支持更一般的东西,比如 CL的符号宏 。在宏扩展时 - 在读取程序之后但在编译和运行之前 - 系统遍历程序的结构,寻找具有宏定义的东西:当它找到它时,它调用与宏相对应的函数使用由其参数指定的源代码,并且宏返回一些其他的源代码块,它们依次行走,直到没有剩下的宏(是的,宏可以扩展到涉及其他宏的代码,甚至代码涉及自己)。完成此过程后,可以(编译并)运行生成的代码。
m
(m ...)
因此,虽然宏看起来像代码中的函数调用,但它们是 不 只是不评估其参数的函数,如FEXPRs:相反,它们是需要一些Lisp源代码并返回另一个Lisp源代码的函数:它们是 语法变换器 ,或对源代码(语法)进行操作并返回其他源代码的函数。宏在宏观扩展时运行,这在评估时间之前是正确的(参见上文)。
所以,实际上是宏 是 用Lisp编写的函数和它们调用的函数完全按照常规方式评估它们的参数:一切都非常普通。但宏的论点是 程式 (或者表示为某种Lisp对象的程序语法),它们的结果是(其语法)其他程序。如果你愿意,宏是元级别的函数。因此宏是一个计算(部分)程序的函数:这些程序以后可能会运行(可能更晚,也许永远不会),此时评估规则将应用于它们。但是,在宏观被调用的时候,它所处理的只是程序的语法,而不是评估该语法的一部分。
所以,我认为你的心理模型是宏就像FEXPRs,在这种情况下,'如何评估参数'问题是一个明显的问题。但它们不是:它们是计算程序的函数,它们在运行计算程序之前运行正常。
对不起,这个答案已经很漫长了。
FEXPR总是很成问题。比如应该做什么 (apply f ...) 做?以来 f 可能是一个FEXPR,但是直到运行时才能知道这一点,很难知道正确的做法是什么。
(apply f ...)
所以我认为发生了两件事:
delay
force
我不知道:我认为它可能是,但它也可能是一个理性的重建。我当然,在非常古老的Lisps中的旧程序中,已经看到FEXPR以我描述的方式使用。我想肯特皮特曼的论文, Lisp中的特殊表格 可能有一些历史:我过去读过它但直到现在才忘记它。
宏定义是转换函数的定义 码 。宏功能的输入是 形式 在宏调用中。宏函数的返回值将被视为 码 插入宏窗体的位置。 Clojure代码由Clojure数据结构(主要是列表,向量和映射)组成。
在你的 foo 宏,你定义宏函数来返回任何东西 blah 做了你的 码 。以来 blah (几乎)是 identity 函数,它只返回它的输入。
blah
identity
您的案例中发生的情况如下:
"(foo (+ 1 2 3))"
(foo (+ 1 2 3))
xs
(+ 1 2 3)
+
如果你想要宏 foo 至 扩大 致电 blah ,你需要返回这样一个表格。 Clojure使用反引号提供模板方便语法,因此您不必使用 list 等等来构建代码:
list
(defmacro foo [xs] `(blah ~xs))
这就像:
(defmacro foo [xs] (list 'blah xs))