宏在未明确要求这样做之前不会评估其参数,但是函数会对其进行评估。在以下代码中:
(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)
绑定到xs
等等。
我基本上只是记住了宏中的辅助函数与参数评估之间的这种交互,但是老实说这违背了我的直觉(xs
会在进入主体之前进行评估,因为函数参数是始终评估)。
我的想法基本上是:“好吧,在这个宏体中,我有xs
作为未求值的列表,但是如果我从宏中调用带有xs
的函数,则应该对该列表求值”。
很明显,我对事情的运作方式有一个令人尴尬的基本误解。我的解释中缺少什么?评估实际上是如何进行的?
编辑
我想我只是对各种术语感到困惑,但是考虑到引号形式与未求值形式同义,并且给定宏参数未求值,则它们被隐式引用。
因此,在我上面的示例中,说xs
未加引号有些误导。例如,此宏:
(defmacro bluh [xs]
`(+ 1 2 ~xs))
与下面的宏基本相同(不包括符号上的命名空间)。在对xs
的调用中解析list
会返回未评估的(带引号?)列表。
(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而不是整个列表。
我不确定为什么我会如此困惑……我理解正确还是完全走错了路?
答案 0 :(得分:3)
宏定义是用于转换 code 的函数的定义。宏函数的输入是宏调用中的 forms 。宏函数的返回值将被视为在宏表格所在位置插入的 code 。 Clojure代码由Clojure数据结构(主要是列表,向量和地图)组成。
在您的foo
宏中,定义宏函数以将blah
所做的一切返回给您的 code 。由于blah
(几乎)是identity
函数,因此它仅返回其输入内容。
您的情况是:
"(foo (+ 1 2 3))"
已被 read 读取,从而生成一个包含两个符号和三个整数的嵌套列表:(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))
答案 1 :(得分:3)
[此答案试图解释为什么不评估其参数的宏和函数为何是不同的东西。我相信这适用于Clojure中的宏,但我不是Clojure的专家。太长了,抱歉。]
我认为您对Lisp所谓的宏和现代Lisps所没有的但以前称为FEXPR的构造感到困惑。
您可能想要两件有趣且不同的事情:
我会按顺序处理它们。
在传统的Lisp中,像(f x y ...)
这样的形式(其中f
是一个函数)将
f
是一个函数,而不是某些特殊事物; f
相对应的函数,并按语言指定的某种顺序(可能是“未指定的顺序”)对x
,y
和其余参数进行求值); f
。最初需要执行步骤(1),因为f
可能是很特殊的事情(例如if
或quote
),并且可能是在(1)也是如此:所有这些以及在(2)中发生的顺序是语言需要定义的内容(或者,对于Scheme而言,请明确地保持未定义状态)。
此顺序,尤其是(2)和(3)的顺序称为应用顺序或急切求值(以下将其称为应用顺序)。
但是还有其他可能性。一种就是不对参数进行求值:调用该函数,并且仅当需要需要时才对它们进行求值。有两种方法可以做到这一点。
第一种方法是定义语言,以使 all 函数以此方式工作。这称为惰性评估或正常顺序评估(以下将其称为“正常顺序”)。在正常的顺序中,语言函数的参数会在需要的时候用魔术来评估。如果不再需要它们,那么可能根本就不会对其进行评估。因此,使用这种语言(我在这里发明了函数定义的语法,以免提交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))))))
如果您尝试以适用顺序语言(例如普通Scheme)使用此版本的Y,它将永远循环。这是Y的适用订单版本:
(define Y
((λ (y)
(λ (f)
(f (λ (x)
(((y y) f) x)))))
(λ (y)
(λ (f)
(f (λ (x)
(((y y) f) x)))))))
您可以看到它是相同的,但是其中存在额外的λ,这些λ实质上“分散了”评估结果以停止其循环。
正常顺序评估的第二种方法是使用一种语言,该语言主要是应用顺序,但是其中有一些特殊的机制来定义不评估其参数的函数。在这种情况下,通常需要某种特殊的机制在函数的主体中说“现在我想要此参数的值”。从历史上讲,这类东西称为FEXPRs,它们存在于某些非常古老的Lisp实现中:Lisp 1.5拥有它们,我认为MACLISP和InterLisp都也具有它们。
在具有FEXPR的应用顺序语言中,您需要以某种方式能够说“现在我要评估这件事”,而且我认为这是一个问题:该事在什么时候决定评估争论?好吧,在一个真正古老的Lisp(纯粹是动态范围内)中,有一个令人作呕的黑客可以这样做:在定义FEXPR时,您可以传入参数的来源,然后,当您想要它的值时,您只需在其上调用EVAL
。那只是一个糟糕的实现,因为这意味着FEXPR永远无法真正正确地编译,并且您必须使用动态范围,因此永远无法真正将变量编译掉。但这就是某些(全部?)早期实现的方式。
但是FEXPR的这种实现有一个惊人的技巧:如果您已将FEXPR作为其参数的来源,并且您知道这是FEXPR的工作方式,那么,它可以在调用{ {1}}:它可以调用源自源的内容上的EVAL
。而且,实际上,获得的“来源”甚至完全不需要严格的Lisp合法性:FEXPR知道如何操作才能制造出这样的东西。这意味着您可以突然以相当通用的方式扩展语言的语法。但是,这样做的代价是您不能编译其中的任何一个:构造的语法必须在运行时进行解释,并且每次调用FEXPR时都会进行转换。
因此,除了使用FEXPR之外,您还可以做其他事情:可以更改评估的工作方式,以便在发生任何其他事情之前,有一个阶段可以遍历代码,并可能将其转换为其他代码(也许是更简单的代码)。而且这种需求只发生一次:一旦代码被转换,那么所产生的东西就可以藏在某个地方,并且转换不需要再次发生。因此,该过程现在看起来像这样:
因此,现在评估过程分为几个“时间”,它们不重叠(或对于特定定义不重叠):
好吧,所有语言的编译器都可能做这样的事情:在将您的源代码实际转换成机器可以理解的东西之前,它们将进行各种源到源的转换。但是这些东西在编译器的胆量之内,并且在某种形式的源表示形式上运行,该表示形式与该编译器是特有的,并且不是由语言定义的。
Lisp向用户打开此过程。该语言具有两个功能,使之成为可能:
作为第二点的示例,请考虑EVAL
:这是名为(in "my.file")
的函数的函数调用,对吗?好吧,可能是:in
几乎可以肯定不是函数调用,而是将(with-open-file (in "my.file") ...)
绑定到文件句柄。
由于该语言的这两个功能(实际上我将不涉及其他一些功能),Lisp可以做的很棒:它可以让该语言的用户编写这些语法转换功能-宏-< em>在便携式Lisp中。
剩下的唯一事情就是决定如何在源代码中标记这些宏。答案与函数相同:定义宏in
时,您将其用作m
(某些Lisps支持更通用的功能,例如CL's symbol macros。在宏扩展时) -在读取程序之后但在(编译并)运行之前–系统遍历程序的结构,以查找具有宏定义的内容:找到它们后,将调用与源代码对应的宏函数由其参数指定的代码,宏将返回其他一些源代码,然后依次遍历,直到没有宏为止(是的,宏可以扩展为涉及其他宏的代码,甚至扩展为涉及自身的代码)。此过程完成后,可以(编译和)运行生成的代码。
因此,尽管宏看起来像代码中的函数调用,但是它们不是 ,只是不评估其参数的函数,例如FEXPR:它们是需要一些Lisp源代码的函数并返回另一段Lisp源代码:它们是语法转换器,或者是对源代码(语法)进行操作并返回其他源代码的函数。宏在宏扩展时间运行,而宏扩展时间恰好在评估时间之前(见上文)。
因此,实际上,宏是用Lisp编写的函数,并且它们调用的函数按常规完美地评估了其参数:一切都非常普通。但是宏的参数是 programs (或表示为某种Lisp对象的程序的语法),其结果是其他程序的(语法)。如果需要,宏是元级别的函数。因此是一个宏,如果该函数计算程序的一部分(这些程序):这些程序以后可能会自己运行(也许以后很多,也许永远不会),然后将评估规则应用于它们。但是,此时宏称为它所处理的只是程序的语法,而不是评估该语法的一部分。
因此,我认为您的思维模式是,宏就像FEXPR一样,在这种情况下,“如何对参数进行求值”问题是很明显的问题。但是它们不是:它们是计算程序的函数,它们在运行所计算的程序之前可以正常运行。
很抱歉,这个答案已经漫长而漫不经心了。
FEXPR总是很成问题。例如,(m ...)
应该做什么?由于(apply f ...)
可能是FEXPR,但这通常要到运行时才能知道,因此很难知道正确的做法。
所以我认为发生了两件事:
f
之类的结构来构建“ promise”,并使用delay
来强制性地评估承诺-由于语言的语义得到了改进,因此可以完全用该语言实现承诺(CL没有承诺,但是实现它们本质上是微不足道的。)我不知道:我认为可能是,但也可能是理性的重建。我当然在非常老的Lisps中非常老的程序中,已经看到FEXPR正在按照我的描述使用。我认为肯特·皮特曼(Kent Pitman)的论文Special Forms in Lisp可能有一些历史:我以前读过它,但直到现在都忘记了。