tcler's blog --- 其实我是一个程序员
Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious.

Lisp Macro 的故事

<Practical Common Lisp> 摘录


C-8.1: Mac 的故事: 只是一个故事

___ 很久以前,一个由 Lisp 程序员组成的公司。那个年代相当久远,所有 Lisp 还没有宏。 每次,任何不能用函数来定义或是用特殊操作符来完成的事情都不得不完全通过手写来实现,这 带来了很大的不便。不幸的是,这个公司的程序员门虽然杰出却非常懒惰。在他们的程序中,当 需要编写大量单调乏味的代码时,他们往往会写下一个注释来描述想要在该位置上编写的代码。 更不幸的是,由于很懒惰,他们也很讨厌回过头去实际编写那些注释所描述的代码。不久,这个 公司就有了一大堆无法运行的程序,因为它们全都是代表着尚需编写代码的注释。

___ 走投无路之下,老板雇佣了一个初级程序员 Mac。他的工作就是找到这些注释,别写所需的 代码,然后再用其替换掉程序中的注释。Mac从未运行过这些程序—程序尚未完成,他当然运 行不了。但就算这些程序完成了,Mac也不知道该用怎样的输入来运行它们。因此,他只是基于 注释的内容来编写他的代码,再将其发还给最初的程序员。

___ 在 Mac 的帮助下,不久之后,所有的程序都完成了,公司通过销售他们赚取了很多钱,并用这 些钱将其程序员团队扩大了一倍。但不知为何,没有人想到要雇佣其他人来帮助 Mac。很快他就 开始单枪匹马地同时协助几十个程序员了。为了避免将他所有的时间都花在搜索源代码的注释 上,Mac 对程序员们使用的编译器做了一个小小的更改。从那以后,只要编译器遇到一个注释, 它就将注释以电子邮件的形式发给他并等待他将替换的代码传送回去。然而,就算有了这个变 化,Mac 也很难跟上程序员的进度。他尽可能小心地工作,但有时,尤其是注释不够清楚时, 他会犯错误。

___ 不过程序员们注意到了,他们将注释写的越精确,Mac 就越有可能发回正确的代码。一天, 一个花费大量时间用文字来描述他想要的代码的程序员,在他的注释里写入了一个可以生成他想要 的代码的 Lisp 程序。这对 Mac 来说很简单;他只需要运行这个程序并将结果发给编译器就好了。

___ 接下来又出现一种创新。有一个程序员在他程序的开始处写了一段备注,其中含有一个函 数定义以及另一个注释,该注释为: “Mac,不要在这里写任何代码,但要把这个函数留给以后 使用,我将在我的其他注释里用到它。” 同一个程序里还有如下注释: “Mac,将这个注释 替换成用符号 x 和 y 作为参数来运行上面提到的那个函数所得到的结果。”

___ 这项技术在几天里就迅速流行起来,多数程序都含有数十个注释,它们定义了那些只被其他注 释中的代码所使用的函数。为了使 Mac 更容易地辨别那些只含有定义而不必立即回复的注释,程序 员们用一个标准前缀来标记它们:“给Mac的定义,仅供阅读。”(Definition for Mac, Read Only.) 由 于程序员们仍然懒惰,这个写法很快简化成 “DEF.MAC.R/O”,接着又被简化为 “DEFMACRO”。

___ 不久以后,这些给 Mac 的注释中再没有实际可读的英语了。Mac 每天要做的事情就是阅读并反 馈那些来自编译器的含有 DEFMACRO 注释的电子邮件,以及调用那些 DEFMACRO 里所定义的函 数。由于注释中的 Lisp 程序做了所有实际的工作,跟上这些电子邮件的进度完全没有问题。Mac 手头 上突然有了大量的时间,可以坐在他的办公室里做那些关于白色沙滩,蓝色海水和鸡尾酒的白日梦了。

___ 几个月后,程序员们意识到已经很长时间没人见过 Mac 了。当他们去他的办公室时,发现 所有东西上都积了薄薄一层灰,一个桌子上还放着几本热带地区的旅行手册,而电脑则是关着 的。但是编译器仍在正常工作—这怎么可能?看起来 Mac 对编译器做了最后一个修改: 现在不 需要用电子邮件将注释发给 Mac 了,编译器将那些 DEFMACRO 中定义的函数保存下来,并 在其被其他注释调用时运行它们。程序员们觉得没有理由告诉老板 Mac 不再来办公室了。因此直 到今天,Mac 还领着薪水,并且时不时地会从某个热带地区给程序员们发一张明信片。

C-8.2 宏展开期和运行期

___ 理解宏的关键在于必须清楚地知道那些生成代码的代码(宏)和那些最终构成程序的代码(所 有其他内容)之间的区别。当编写宏时,你是在编写那些将被编译器用来生成代码并随后编译的 程序。只有当所有的宏都被完全展开并且产生的代码被编译后,程序才可以实际运行。宏运行的 时期被称为宏展开期(macro expansion time),这个运行期(runtime)是不同的,后者是正常的 代码(包括那些由宏生成的代码)实际运行的阶段。