『阿男的编程本质论』*10 Eval,Macro,Preprocessor,Homoiconicity(三)*
那么Clojure这种macro和C语言里面#include
这种macro有什么区别呢?C语言里面的macro实际上就是一种文本替换,比如#include "foo.h"
在编译的时候就是会把foo.h
里面的内容替换到#include "foo.h"
这里。
因此C语言的preprocessor处理它的macro的时候,就是简单的文本替换。开源的preprocessor,m4
就是这样的工具。当然m4
这种macro expander也支持一些条件语句来进行一定限度上的根据字串里面的一些特征进行有条件替换,但这毕竟还是把输入作为字符串,而不是结构,来进行处理,这种替换方式必定有局限性。
我们可以想一下编译原理相关知识,在Parser处理代码之前,有Tokenizer(也叫Lexer)会把代码转化成token,这种转化,就是把文本转化成结构的基本单元token。这样的话,Parser才能更好地分析语法规则,把tokens联系在一起,变成树形结构AST。因此"结构"在这里面起了很大的作用。
而Clojure里面的macro则是有效的语法结构,本身它的代码就是list数据,list数据本身也可以表示树形结构(通过nested的括号),因此可以说Clojure的Lexer和Parser要做的事情被大大简化了,当然也导致了Clojure了Lisp这种语言的语法显得比较"原始"。但是在上篇文章里,我们也看到了,Clojure的macro功能非常灵活,我们可以直接把传入macro的list数据(也即是代码)进行各种改写,而且这种改写不是简单地文本替换,而是对list数据的操作。
此外,Clojure的macro展开是在compile过程中完成,因此如果我们的macro写的有问题,在compile过程中就会给出报错和相关错误位置。
我们刚才也说了,C语言也有macro的概念,比如#include <stdio.h>
,这种macro也是在compile时展开,但是C语言的这种macro就是文本替换,比如这个#include <stdio.h>
在compile过程中,就是被替换成stdio.h
文件里面的内容。
这种macro展开有什么问题?阿男觉得最大的问题就是,如果你的macro里面被替换过来的代码有bug,compiler在compile过程时拋给你的错误往往特别难以理解,因为我们的macro实际上已经被展开成了完全不同的内容。如果这个内容里面有错误,compiler会报相关的错误,但是做为程序员,这个错误从哪里来的成了一个问题。
比如stdio.h
假设有代码bug,那么你的代码如果使用#include
引用了stdio.h
,那么compiler会在compile的时候把stdio.h
的具体内容替换到#include <stdio.h>
的位置。这样的话,当stdio.h
里面有bug的时候,compiler会报具体的bug。但你看这个bug就会很头疼,因为那个具体的bug根本就不在你的代码里面而是在stdio.h
里面,这就造成理解上的困难。所以C语言这种基于文本替换的macro展开,和Clojure这种代码即数据的macro展开是完全不同的。