Sol*_*xun 5 lisp macros clojure
宏在明确告知之前不会评估它们的参数,但是函数会这样做。在以下代码中:
(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))
Run Code Online (Sandbox Code Playgroud)
似乎blah没有评估xs,因为我们仍然有整个列表:(+ 1 2 3)绑定到xsblah 的正文中。
我基本上只是记住了宏中的辅助函数和它们对参数的评估之间的这种相互作用,但老实说,这违背了我的直觉(即 xs在进入主体之前会进行评估,因为函数参数总是被评估)。
我的想法基本上是:“好吧,在这个宏体中,我有xs一个未xs评估的列表,但是如果我从宏内部调用一个函数,它应该评估该列表”。
显然,我对事情的运作方式有一个令人尴尬的根本误解。我的解释中缺少什么?评估实际上是如何发生的?
编辑
我想我只是混淆了各种术语,但考虑到引用形式与未评估形式同义,并且给定宏参数未评估,它们被隐式引用。
所以在我上面的例子中,说xsis unquoted 有点误导。例如,这个宏:
(defmacro bluh [xs]
`(+ 1 2 ~xs))
Run Code Online (Sandbox Code Playgroud)
与下面的宏基本相同(不包括符号上的命名空间)。xs在调用中解析list会返回一个未评估(引用?)的列表。
(defmacro bleh [xs]
(list '+ '1 '2 xs)) ;; xs resolves to a quoted list (or effectively quoted)
Run Code Online (Sandbox Code Playgroud)
调用 bleh(或 bluh)等同于说:
(list '+ '1 '2 '(+ 1 2 3))
;; => (+ 1 2 (+ 1 2 3))
Run Code Online (Sandbox Code Playgroud)
如果xs没有解析为引用列表,那么我们最终会得到:
(list '+ '1 '2 (+ 1 2 3))
;; => (+ 1 2 6)
Run Code Online (Sandbox Code Playgroud)
因此,简而言之,宏参数被引用。
我认为我的困惑部分来自于将语法引用的表单视为模板,其中填充了插槽,例如(+ 1 2 ~xs)我会在精神上扩展为(+ 1 2 (+ 1 2 3)),并且看到(+ 1 2 3)在扩展中没有引用它,我发现函数调用使用xs(在第一个示例中)以上blah)不会立即评估6。
模板比喻很有帮助,但如果我将其视为快捷方式,(list '+ '1 '2 xs)很明显xs必须是引用列表,否则扩展将包含 6 而不是整个列表。
我不知道为什么我觉得这如此令人困惑……我做对了还是完全走错了路?
小智 6
[这个答案试图解释为什么不评估其参数的宏和函数是不同的东西。我相信这适用于 Clojure 中的宏,但我不是 Clojure 的专家。也太长了,抱歉。]
我认为您对 Lisp 所称的宏和现代 Lisp 没有但曾经被称为 FEXPR 的构造感到困惑。
您可能想要两件有趣的、不同的东西:
我会按顺序处理它们。
在传统的 Lisp 中,像(f x y ...), where fis a function这样的形式将:
f是一个功能而不是一些特殊的东西;f并评估x, y, & 其余参数的函数,以语言指定的某种顺序(可能是“以未指定的顺序”);f参数的评估结果。步骤(1)首先需要的,因为f可能是一件特殊的事情(比如说if,或quote),它可能是功能定义在(1)中检索,以及:这一切,还有为了使事情发生in in (2) 是语言需要定义的东西(或者,在 Scheme 的情况下,明确未定义)。
这种排序,特别是 (2) 和 (3) 的排序被称为应用顺序或急切求值(我将在下面称之为应用顺序)。
但还有其他可能性。其中之一是不评估参数:调用函数,并且仅当需要参数的值时才对它们进行评估。有两种方法可以做到这一点。
第一种方法是定义语言,以便所有函数都以这种方式工作。这称为惰性求值或正常顺序求值(下面我将其称为正常顺序)。在正常的顺序语言中,函数参数在需要时被魔法评估。如果永远不需要它们,那么它们可能永远不会被评估。所以在这样一种语言中(我在这里发明了函数定义的语法,以免提交 CL 或 Clojure 或其他任何东西):
(def foo (x y z)
(if x y z))
Run Code Online (Sandbox Code Playgroud)
在对 的调用中只会评估y或之一。zfoo
在正常的顺序语言中,您不需要明确关心何时评估事物:该语言确保在需要时对它们进行评估。
正常顺序语言似乎是一个明显的胜利,但我认为它们往往很难使用。有两个问题,一个明显,一个不那么明显:
副作用问题可以被视为一个非问题:我们都知道有副作用的代码很糟糕,对吧,那么谁在乎呢?但即使没有副作用,情况也有所不同。例如,这里是正常顺序语言中 Y 组合子的定义(这是 Scheme 的一种非常简单的正常顺序子集):
(define Y
((? (y)
(? (f)
(f ((y y) f))))
(? (y)
(? (f)
(f ((y y) f))))))
Run Code Online (Sandbox Code Playgroud)
如果您尝试在应用顺序语言中使用这个版本的 Y——就像普通的 Scheme——它将永远循环。这是 Y 的适用顺序版本:
(define Y
((? (y)
(? (f)
(f (? (x)
(((y y) f) x)))))
(? (y)
(? (f)
(f (? (x)
(((y y) f) x)))))))
Run Code Online (Sandbox Code Playgroud)
你可以看到它有点相同,但是那里有额外的 ?s 基本上“延迟”评估以阻止它循环。
正常顺序评估的第二种方法是拥有一种主要是应用顺序的语言,但其中有一些特殊的机制来定义不评估其参数的函数。在这种情况下,通常需要一些特殊的机制来在函数体中说“现在我想要这个参数的值”。历史上这样的东西被称为FEXPRs,它们存在于一些非常古老的 Lisp 实现中:Lisp 1.5 有它们,我认为 MACLISP 和 InterLisp 也有它们。
在带有 FEXPR 的应用顺序语言中,您需要以某种方式能够说“现在我想评估这个东西”,我认为这是遇到的问题:这个东西在什么时候决定评估参数?好吧,在一个真正的纯动态范围的 Lisp 中,有一个令人作呕的 hack 来做到这一点:在定义 FEXPR 时,你可以只传递参数的源,然后,当你想要它的值时,你只需调用EVAL它。这只是一个糟糕的实现,因为这意味着 FEXPR 永远无法真正正确编译,并且您必须使用动态范围,因此永远无法真正编译掉变量。但这就是一些(全部?)早期实现的方式。
但是 FEXPRs 的这种实现允许一个惊人的 hack:如果你有一个 FEXPR 已经给出了它的参数的来源,并且你知道这就是 FEXPRs 的工作方式,那么它可以在调用EVAL它之前操纵它:它可以调用EVAL从源中派生的东西。而且,事实上,它得到的“来源”甚至根本不需要是严格合法的 Lisp:它可以是 FEXPR 知道如何操纵以制造某种东西的东西。这意味着您可以突然以非常通用的方式扩展语言的语法。但是能够做到这一点的代价是您无法编译其中任何一个:您构造的语法必须在运行时解释,并且每次调用 FEXPR 时都会发生转换。
因此,除了使用 FEXPR,您还可以做其他事情:您可以更改评估的工作方式,以便在其他任何事情发生之前,有一个阶段,在此阶段代码被遍历并可能转换为其他一些代码(更简单的代码) , 也许)。而且这种需要只发生一次:一旦代码被转换,结果就可以藏在某个地方,转换不需要再次发生。所以这个过程现在看起来像这样:
因此,现在评估过程分为几个“时间”,它们不重叠(或对于特定定义不重叠):
好吧,所有语言的编译器可能会做这样的事情:在实际将您的源代码转换为机器理解的内容之前,它们会进行各种源到源转换。但是这些东西在编译器的内部,并且正在对源代码的某种表示进行操作,这些表示对于该编译器来说是特殊的,而不是由语言定义的。
Lisp 向用户开放这个过程。该语言有两个特性使这成为可能:
作为第二点的一个例子,请考虑(in "my.file"):这是一个名为 的函数的函数调用in,对吗?好吧,可能是:(with-open-file (in "my.file") ...)几乎可以肯定不是函数调用,而是绑定in到文件句柄。
由于该语言的这两个特性(实际上还有一些我不会介绍),Lisp 可以做一件很棒的事情:它可以让该语言的用户在可移植的 Lisp 中编写这些语法转换函数——宏。
剩下的唯一事情就是决定如何在源代码中标记这些宏。答案与函数相同:当你定义一些宏时,m你就像使用它一样(m ...)(一些 Lisps 支持更一般的东西,比如CL 的符号宏. 在宏扩展时——在程序被读取之后但在它(编译和)运行之前——系统遍历程序的结构,寻找具有宏定义的东西:当它找到它们时,它调用对应于宏的函数使用由其参数指定的源代码,并且宏返回一些其他源代码块,依次遍历直到没有宏为止(是的,宏可以扩展到涉及其他宏的代码,甚至涉及到自己的代码)。一旦此过程完成,就可以(编译和)运行生成的代码。
因此,尽管宏观样子在代码的函数调用,他们是不是不评价他们的论点,像FEXPRs只是功能:代替它们是需要一点Lisp的源代码,并返回的Lisp语言源代码另一个位功能:它们“重新语法变压器,或功能,其上的源代码操作(语法)和返回其他的源代码。宏在宏扩展时间运行,这正好在评估时间之前(见上文)。
所以,实际上宏是用 Lisp 编写的函数,它们调用的函数完全按照惯例评估它们的参数:一切都非常普通。但是宏的参数是程序(或表示为某种 Lisp 对象的程序语法),它们的结果是其他程序(的语法)。如果您愿意,宏是元级别的函数。因此,如果一个计算(部分)程序的函数是一个宏:这些程序可能稍后会自己运行(可能会更晚,可能永远不会),此时评估规则将应用于它们。但在这一点上,宏被称为它所处理的只是程序的语法,而不是评估该语法的一部分。
因此,我认为您的心智模型是宏类似于 FEXPR,在这种情况下,“如何评估参数”问题是显而易见的。但它们不是:它们是计算程序的函数,并且在它们计算的程序运行之前它们就可以正常运行。
对不起,这个答案太长而且漫无边际。
FEXPR 总是很成问题。比如应该(apply f ...)怎么做?由于f可能是 FEXPR,但这通常在运行时之前无法知道,因此很难知道正确的做法是什么。
所以我认为发生了两件事:
delay构造“承诺”和force强制评估承诺之类的结构来实现——因为语言的语义得到了改进,这成为可能完全在语言中实现承诺(CL 没有承诺,但实现它们本质上是微不足道的)。我不知道:我认为可能是但也可能是理性的重构。我当然,在非常古老的 Lisps 中的非常古老的程序中,已经看到 FEXPR 以我描述的方式被使用。我认为 Kent Pitman 的论文Lisp 中的特殊形式可能有一些历史:我过去读过,但直到现在才忘记。
宏定义是转换代码的函数的定义。宏函数的输入是宏调用中的形式。宏函数的返回值将被视为插入宏形式所在位置的代码。Clojure 代码由 Clojure 数据结构(主要是列表、向量和映射)组成。
\n\n在宏中foo,您定义宏函数以返回blah对代码执行的任何操作。由于blahis(几乎)是identity函数,因此它只返回其输入的内容。
您的情况发生的情况如下:
\n\n"(foo (+ 1 2 3))"(foo (+ 1 2 3))foo符号被解析为宏foo。foo调用宏函数时,其参数xs绑定到列表(+ 1 2 3)。blah宏函数(打印然后)使用列表调用函数。blah(打印然后)返回该列表。(+ 1 2 3)。+被解析为加法函数。如果您希望宏foo扩展为对 的调用,则需要返回这样的形式。Clojure 提供了使用反引号的模板化便利语法,这样您就不必使用etc. 来构建代码:blahlist
(defmacro foo [xs]\n `(blah ~xs))\nRun Code Online (Sandbox Code Playgroud)\n\n就像:
\n\n(defmacro foo [xs]\n (list \'blah xs))\nRun Code Online (Sandbox Code Playgroud)\n
| 归档时间: |
|
| 查看次数: |
673 次 |
| 最近记录: |