宏调用的函数中参数的求值

时间:2019-01-28 02:55:25

标签: clojure macros lisp

宏在未明确要求这样做之前不会评估其参数,但是函数会对其进行评估。在以下代码中:

(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而不是整个列表。

我不确定为什么我会如此困惑……我理解正确还是完全走错了路?

2 个答案:

答案 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中称为 macros

我会按顺序处理它们。

不能立即求值的函数

在传统的Lisp中,像(f x y ...)这样的形式(其中f是一个函数)将

  1. 确定f是一个函数,而不是某些特殊事物;
  2. 获取与f相对应的函数,并按语言指定的某种顺序(可能是“未指定的顺序”)对xy和其余参数进行求值);
  3. 使用评估参数的结果调用f

最初需要执行步骤(1),因为f可能是很特殊的事情(例如ifquote),并且可能是在(1)也是如此:所有这些以及在(2)中发生的顺序是语言需要定义的内容(或者,对于Scheme而言,请明确地保持未定义状态)。

此顺序,尤其是(2)和(3)的顺序称为应用顺序急切求值(以下将其称为应用顺序)。

但是还有其他可能性。一种就是不对参数进行求值:调用该函数,并且仅当需要需要时才对它们进行求值。有两种方法可以做到这一点。

第一种方法是定义语言,以使 all 函数以此方式工作。这称为惰性评估正常顺序评估(以下将其称为“正常顺序”)。在正常的顺序中,语言函数的参数会在需要的时候用魔术来评估。如果不再需要它们,那么可能根本就不会对其进行评估。因此,使用这种语言(我在这里发明了函数定义的语法,以免提交CL或Clojure或其他任何东西):

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

在调用y时将只评估zfoo中的一个。

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

普通订单语言似乎是一个明显的胜利,但我认为它们往往很难使用。有两个问题,一个很明显,一个不那么明显:

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

副作用问题可以看作是一个非问题:我们都知道带有副作用的代码是不好的,对,那么谁在乎呢?但是即使没有副作用,情况也有所不同。例如,以下是正常顺序语言中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之外,您还可以做其他事情:可以更改评估的工作方式,以便在发生任何其他事情之前,有一个阶段可以遍历代码,并可能将其转换为其他代码(也许是更简单的代码)。而且这种需求只发生一次:一旦代码被转换,那么所产生的东西就可以藏在某个地方,并且转换不需要再次发生。因此,该过程现在看起来像这样:

  1. 代码被读入并从中构建结构;
  2. 此初始结构可能会转换为其他结构;
  3. (可能会编译结果结构);
  4. 对结果结构或对其进行编译的结果可能会进行多次评估。

因此,现在评估过程分为几个“时间”,它们不重叠(或对于特定定义不重叠):

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

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

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发生了什么事?

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

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

  • 在人们真的想要普通命令语言的情况下,他们实施了这些命令,并且对于那些语言,评估规则处理了FEXPR试图解决的问题;
  • 使用适用顺序语言,然后,如果您不想评估某些参数,则可以通过明确地说使用f之类的结构来构建“ promise”,并使用delay来强制性地评估承诺-由于语言的语义得到了改进,因此可以完全用该语言实现承诺(CL没有承诺,但是实现它们本质上是微不足道的。)

我描述的历史记录正确吗?

我不知道:我认为可能是,但也可能是理性的重建。我当然在非常老的Lisps中非常老的程序中,已经看到FEXPR正在按照我的描述使用。我认为肯特·皮特曼(Kent Pitman)的论文Special Forms in Lisp可能有一些历史:我以前读过它,但直到现在都忘记了。