Search This Blog

Tuesday, February 16, 2010

一个关于macro的故事

很久很久以前,有一个由lisp程序员组成的公司。这个公司所处的时代太早,以至于当时的lisp还没有macro。每当有不能用函数来定义或者特殊操作符来实现的东西,就得全部手工写,这是相当无聊的事。不幸的是,这个公司的程序员虽然都很有才华,同时却又灰常懒。在写程序的时候,当他们觉得写一长串代码实在太麻烦了,就会在程序里写上描述他们实际上所需要写的代码的记录。更不幸的是,这些人实在太懒了,所以他们也痛恨回过头去完成那些记录所描述的代码。没过多久,公司就拥有了一堆没人能运行地起来的程序,因为这些程序充斥着关于未完成的代码的记录。

大老板很绝望,请了一个名叫Mac的不那么牛逼的程序员,让他从程序里找出记录,写出所需的代码,然后把代码插入程序以代替记录。Mac从来不运行那些程序,因为很显然,程序还没完成,所以他没法运行。然而就算程序完成了,Mac也不知道应该给它们提供什么输入。所以他只是根据记录的内容写出代码,然后发给原作者。

在Mac的帮助下,很快所有的程序都完成了,公司卖掉这些程序,赚了一大笔钱,足够把开发人数加倍。但是不知道怎么回事,没人想到要再雇一个人帮Mac;很快,出现了Mac一个人协助一大坨程序员的状况。为了避免把所有的时间都花在找记录上,Mac对程序员们所用的编译器做了一点改动,这样,任何时候编译器遇到一个记录,就会通过email发给Mac,然后等Mac把代码发回来代替记录。悲剧的是,即使这样,Mac也很难赶上这么多程序员的进度。他尽可能认真地工作,但是有时候——尤其是记录写得不是很明确的情况下——他会犯错误。

然而程序员们注意到,如果他们的记录写地越精确,那么Mac就越能发回正确的代码。有一天,一个程序员写记录写得很痛苦,于是在一个记录里写了一段能生成他所需要代码的lisp代码。这下爽了Mac,他运行了这段代码,然后把结果送给编译器。

下一个创新是,一个程序员在他的一个程序里放了一个记录,包括一个函数定义和一段注释:"Mac,这里不用写代码,留着这个函数就行,我在别的记录里要用到这个函数。"这个程序里别的记录则是这样的:"Mac,用符号x和y作为参数运行刚才那个函数,返回的结果替代这个记录。"

这项技术流行地很快,没过几天,大部分程序都包含了很多定义函数的记录,而这些函数在其他记录中被使用。为了让Mac容易挑选只包含定义而不需要马上响应的记录,程序员们为这种记录加上了标准的开头:"Definition for Mac,read only",而这帮程序员又太懒,所以很快缩短为"DEF.MAC R/O",接着又变成"DEFMACRO"。

没过多久,给Mac的记录里已经没有真正的英语了。他每天所做的事就是接受、回复来自编译器的邮件,这些邮件是包含DEFMACRO的记录或者是对DEFMACRO里的函数的调用。既然记录里的lisp代码做了真正的工作,那么赶上email的进度就没那么困难了。Mac突然有了大把的时间可以坐在办公室里做着关于白色沙滩、蓝色海水和放着小纸伞的饮料(貌似是鸡尾酒?)的白日梦。

几个月以后,程序员们意识到已经有段时间没人见到过Mac了。当他们去了他的办公室,发现所有的东西上都蒙了一层薄薄的灰尘,桌子上放着一堆关于热带地区旅游的小册子,而电脑是关着的。但是编译器正常工作着啊,怎么可能呢?最后他们发现Mac给编译器做了在最后一个改动:编译器不再用email给Mac发记录,而是把DEFMACRO里定义的函数保存下来,当其他记录调用的时候就运行这些函数。程序员们觉得木有必要告诉大老板Mac再也不用来办公室了,所以Mac至今还领着薪水,时不时地从各种不同的热带场所给程序员们发张贺卡来。

*****************************华丽的分割线***************************

这个故事是《practical common lisp》第八章讲到的,我翻译地不好,不过领会精神是够了。

先看不相关的,懒惰是美德。故事里的人物,各个都懒得出奇,精英程序员们太过牛逼,以至于写代码都懒,直接写上伪代码了事。Mac虽然是junior programmer,但是一旦抓到机会,就会想到办法让机器帮他做事。而大老板,作为一个万恶的资本家,竟然连自己花钱雇用的工人光拿钱不干活也不知道,莫非就是每天打打高尔乎,心情好了就去公司闪现一下?哈哈。但是就是这么一帮人,也能挣大钱,也能创新,事实上,创新恰恰来自于懒惰,值得大家学习。

然后是一点关于macro的东西,主要是针对非科班出身又对编译原理很恐惧的同学。但凡编程语言,都不会对使用者掌握编译原理有明确的要求,但同时,诸如"掌握编译原理对写出正确、高效的代码至关重要"这类的言论也是非常多的。编译是一个灰常可怕的过程,如果说通过那门课程是皮毛的话,那我是只有皮木有毛。这里结合我自己的体会,讲讲macro,达人请无视。

不少童鞋初学编程语言,拿了书上的例子来跑,这样很好,动手才是王道。但是相当多的人,比如说学C,课后习题都做完了,却不知道自己的代码是怎么在机器上跑的,这样就很不好。下面是编译型语言的基本过程:

1 把代码敲入编辑器(editor),保存。
2 调用编译器(compiler)把源代码编译成可执行程序。
3 运行程序。

IDE会把编辑器和编译器以及调试器整成一坨,这样方便了开发者,却不利于初学者搞懂整个过程,以至于有些童鞋连编辑器和编译器都会混淆。在这里顺便鄙视一下某公司的Visual叉叉叉,我有一次试图写个程序,却被一大堆的菜单彻底弄晕了,以至于我至今不会用visual叉叉叉写程序。

而编译再分成几步,以C为例:

1 头文件插入,宏替换
2 词法分析
3 语法分析
4 中间代码生成
5 生成汇编代码
6 汇编,生成目标文件
7 链接,生成可执行文件

很多童鞋搞不清宏到底是怎么回事,从上面的过程可以看到,宏替换发生在第一步(严格来说,这一步不算在编译里),而你在代码里所写的函数是在你执行了第七步里生成的可执行文件以后才会被调用,这就是一个运行时的概念。所以宏和其他的代码的区别就很明显了:宏只在编译之前存在,编译以后,宏被展开,变成具体的代码,自己也消失了。

因为事实上C的macro基本上只是简单的替换,而common lisp的macro更为强大,所以主要还是八卦一下common lisp的macro吧。

其实开头的故事把macro和function的区别讲得很清楚,Mac没法运行程序,因为他工作的阶段是编译时,更确切地讲是宏展开时,而不是运行时,所以整个程序运行所依赖的环境还没建立起来,所以他根本不知道该给函数什么样的输入。而其他的精英程序员们后来在DEFMACRO里的函数其实就是macro,作用就是生成代码,Mac在调用DEFMACRO里的函数的时候,给的参数是精英程序员指定的,比如x和y,这两个参数不是来自于运行时环境,当编译完成后,DEFMACRO里的函数也就没有用了,因为它们已经根据参数生成了具体的代码,在运行时执行的则是具体的代码。大部分情况下,参数都是符号,而不是具体的变量,所以macro其实会涉及到很多的符号计算。


很多人说common lisp的macro太强大了,以至于跟C的macro根本不是一个东西。其实我还不是很理解这种说法。就我自己的体会,common lisp的macro强大在defmacro有整个语言的核心的支持,而C的macro并不作为编译器的一部分存在,而只是预处理器,并且也没有C语言的核心的支持,只有很简单的计算能力。至于在内部实现上,暂时我还没这个能力去探究,任重而道远啊。

3 comments:

Anonymous said...
This comment has been removed by a blog administrator.
Tony Ng 吳時輝 said...
This comment has been removed by a blog administrator.
Tony Ng 吳時輝 said...
This comment has been removed by a blog administrator.