Lisp宏能做什么是“一等函数”无法做到的?
我觉得我明白Lisp中的宏以及它们在编译阶段的作用。
但是在Python中,你可以把一个函数作为参数传递给另一个函数。
def f(filename, g):
try:
fh = open(filename, "rb")
g(fh)
finally:
close(fh)
所以,在这里我们可以实现懒惰求值。那么,宏能做的事情,函数作为一等公民就做不了什么呢?
8 个回答
宏可以改变代码
宏的作用是对源代码进行转换。懒惰求值则不一样。想象一下,你现在可以编写函数,将任意代码转换成不同的代码。
非常简单的代码转换
创建简单的语言结构只是一个非常简单的例子。考虑一下打开文件的例子:
(with-open-file (stream file :direction :input)
(do-something stream))
和
(call-with-stream (function do-something)
file
:direction :input)
宏给我的,是稍微不同的语法和代码结构。
嵌入式语言:高级迭代结构
接下来考虑一个稍微不同的例子:
(loop for i from 10 below 20 collect (sqr i))
和
(collect-for 10 20 (function sqr))
我们可以定义一个函数 COLLECT-FOR
,它可以在简单循环中做同样的事情,并且有起始、结束和步进函数的变量。
但是 LOOP
提供了一种新的语言。LOOP
宏就是这个语言的编译器。这个编译器可以进行 LOOP
特有的优化,并且可以在编译时检查这种新语言的语法。一个更强大的循环宏是 ITERATE
。这些强大的语言级工具现在可以作为库编写,而不需要特别的编译器支持。
在宏中遍历代码树并进行修改
接下来是另一个简单的例子:
(with-slots (age name) some-person
(print name)
(princ " "
(princ age))
和类似的东西:
(flet ((age (person) (slot-value person 'age))
(name (person) (slot-value person 'name)))
(print (name))
(princ " ")
(princ (age)))
WITH-SLOTS
宏会遍历整个包含的源代码树,并将变量 name
替换为 (SLOT-VALUE SOME-PERSON 'name)
的调用:
(progn
(print (slot-value some-person 'name))
(princ " "
(princ (slot-value some-person 'age)))
在这个例子中,宏可以重写代码的特定部分。它理解 Lisp 语言的结构,并知道 name
和 age
是变量。它还理解在某些情况下 name
和 age
可能不是变量,因此不应该被重写。这是一个所谓的 代码遍历器 的应用,它可以遍历代码树并对其进行修改。
宏可以修改编译时环境
另一个简单的例子,是一个小文件的内容:
(defmacro oneplus (x)
(print (list 'expanding 'oneplus 'with x))
`(1+ ,x))
(defun example (a b)
(+ (oneplus a) (oneplus (* a b))))
在这个例子中,我们不关注宏 ONEPLUS
,而是关注宏 DEFMACRO
本身。
它有什么有趣的地方?在 Lisp 中,你可以有一个包含上述内容的文件,并使用 文件编译器 来编译这个文件。
;;; Compiling file /private/tmp/test.lisp ...
;;; Safety = 3, Speed = 1, Space = 1, Float = 1, Interruptible = 1
;;; Compilation speed = 1, Debug = 2, Fixnum safety = 3
;;; Source level debugging is on
;;; Source file recording is on
;;; Cross referencing is on
; (TOP-LEVEL-FORM 0)
; ONEPLUS
(EXPANDING ONEPLUS SOURCE A)
(EXPANDING ONEPLUS SOURCE (* A B))
; EXAMPLE
;; Processing Cross Reference Information
所以我们看到,文件编译器 扩展了对 ONEPLUS
宏的使用。
这有什么特别之处?文件中有一个宏定义,而在下一个形式中我们已经使用了这个新宏 ONEPLUS
。我们从未将宏定义加载到 Lisp 中。编译器以某种方式知道并注册了定义的宏 ONEPLUS
,然后能够使用它。
因此,宏 DEFMACRO
在编译时环境中注册了新定义的宏 ONEPLUS
,使得编译器知道这个宏——而无需加载代码。这个宏可以在宏展开时在编译时执行。
使用函数我们无法做到这一点。编译器为函数调用生成代码,但不执行它们。但宏可以在编译时运行,并为编译器添加“知识”。这种知识在编译器运行期间有效,之后部分会被遗忘。DEFMACRO
是一个在编译时执行的宏,然后通知编译时环境一个新宏的存在。
还要注意,宏 ONEPLUS
也会运行两次,因为它在文件中使用了两次。副作用是它会打印一些东西。但 ONEPLUS
也可能有其他任意副作用。例如,它可以检查包含的源代码是否符合某些规则,并在代码违反规则时提醒你(想想样式检查器)。
这意味着,宏,这里是 DEFMACRO
,可以在编译文件时改变语言及其环境。在其他语言中,编译器可能提供特殊的编译指令,这些指令在编译时会被识别。有很多这样的定义宏影响编译器的例子:DEFUN
、DEFCLASS
、DEFMETHOD
等等。
宏可以让用户代码更简洁
一个典型的例子是 DEFSTRUCT
宏,用于定义类似于记录的数据结构。
(defstruct person name age salary)
上面的 defstruct
宏创建了以下代码:
- 一个新的结构类型
person
,有三个槽 - 用于读取和写入值的槽访问器
- 一个检查某个对象是否属于
person
类的谓词 - 一个创建结构对象的
make-person
函数 - 一个打印表示
此外,它可能会:
- 记录源代码
- 记录源代码的来源(文件、编辑器缓冲区、REPL 等)
- 交叉引用源代码
定义结构的原始代码是一行简短的代码,而扩展后的代码则要长得多。
DEFSTRUCT
宏不需要访问语言的元级别来创建这些不同的东西。它只是将一小段描述性代码转换为通常更长的定义代码,使用典型的语言结构。
这是Matthias Felleisen在2002年的回答(通过http://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg01539.html):
我想提议,宏有三种有条理的使用方式:
数据子语言:我可以写看起来简单的表达式,然后用宏整齐地创建复杂的嵌套列表、数组或表格,像是用引号、取消引号等。
绑定结构:我可以用宏引入新的绑定结构。这可以帮助我摆脱lambda表达式,并把属于同一组的东西放得更近。例如,我们的一个教学包里有一个表单
(web-query ([last-name (string-append "你好 " first-name " 你的姓是什么?"]) ... last-name ... first-name ...) 这暗示了程序和网络用户之间的明显互动。
[注意:在ML中,你可以写 web-query(fn last-name => ...)string_append(...),但这实在是麻烦且不必要的模式。]评估重排序:我可以引入一些结构,按需延迟或推迟表达式的评估。想想循环、新的条件语句、延迟/强制等。
[注意:在Haskell中,你不需要这个。]我知道Lisp程序员使用宏是出于其他原因。老实说,我认为这部分是因为编译器的不足,部分是因为目标语言中的“语义”不规则性。
我挑战大家在说语言X可以做到宏能做到的事情时,考虑这三个问题。
-- Matthias
Felleisen是宏领域最有影响力的研究者之一。(不过我不知道他现在是否还同意这个观点。)
更多阅读:Paul Graham的《On Lisp》(http://www.paulgraham.com/onlisp.html; Graham 绝对不同意Felleisen认为这些是宏唯一有用的用法),以及Shriram Krishnamurthi的论文《通过宏实现自动机》(http://www.cs.brown.edu/~sk/Publications/Papers/Published/sk-automata-macros/)。
首先,Lisp 也有一等函数,所以你也可以问:“如果我已经有一等函数,为什么还需要宏?”答案是,一等函数无法让你玩弄语法。
从表面上看,一等函数让你可以写 f(filename, some_function)
或 f(filename, lambda fh: fh.whatever(x))
,但不能写 f(filename, fh, fh.whatever(x))
。虽然这样说可能有点道理,因为在最后一种情况下,fh
突然出现的地方就不太清楚了。
更重要的是,函数只能包含有效的代码。所以你不能写一个高阶函数 reverse_function
,让它接受一个函数作为参数,然后“反向”执行它,比如 reverse_function(lambda: "hello world" print)
会执行 print "hello world"
。但用宏你就可以做到。当然,这个例子有点傻,但这个能力在嵌入特定领域语言时非常有用。
例如,你无法在 Python 中实现 Common Lisp 的 loop
结构。实际上,如果 Python 的 for ... in
结构不是内置的,你甚至无法用 Python 自己实现它——至少不能用那种语法。当然,你可以实现类似 for(collection, function)
的东西,但那就没那么好看了。