对于 Scheme 语言的初学者而言,Scheme 的宏彷佛永远是他们津津乐道的重要特性之一。譬如,我在上一章的结尾处说过,『也许不会再有比 Scheme 更高层次的编程语言了。虽然人类的大脑依然在源源不断的构造着抽象之抽象的概念,可是 Scheme 自身能够随之进化——经过宏来定义新的语法』。这句话的背景彷佛很是宏伟,但我确信它是初学者的言论。若是稍微考察一下汇编语言,不难发现,汇编语言的宏也具有与 Scheme 宏的类似的特性。程序员
对于 Guile 而言,它所实现的 Scheme 标准以及 Guile 自家的一些模块已经为咱们定义了足够用的宏了。在咱们的程序里,几乎不须要触及宏。本章之因此要讲述宏的基本知识,用意在于揭示宏是一种很简单的编程范式,从而消除本身对宏的过分崇拜或恐惧之心。编程
不管是过程式编程,面向对象编程,泛型编程,函数式编程,仍是搞明白范畴论以后再编程,所要解决的基本问题是怎样更有效的复用既有代码。segmentfault
最原始的代码复用方式是 Copy & Paste。这种最原始的代码复用方式为程序的 Bug 的繁衍作出了不可磨灭的贡献,也许如今它还在兢兢业业的创造 Bug。不然,程序员们不会成天将 DRY(Do not Repeat Yourself)挂在嘴边。安全
为了消除代码块的 Copy & Paste 带来的 Bug,有一些程序员开窍了,写出来宏处理器。这样,就能够将重复使用的代码块单独抽离出来,给它取个名字,而后将代码块中须要变化的部分用一些占位符代替,并将占位符做为宏的形式参数。因而,就能够将代码块转化为模板之类的东西。宏,本质上就是一种简单可是自由的代码模板,它对模板参数不作任何检查。当一个宏被调用时,展开所得的代码块中的占位符会被宏处理器替换为这个宏所接受的参数。若是宏的参数有反作用,一般会在宏的展开结果中创造出难以察觉的 Bug。不过,这总比 Copy & Paste 安全多了,并且也高效多了。框架
C++ 的模板比宏高级了一些,但这是以大幅度牺牲宏的自由性而且大幅度增长编译器的复杂性为代价的。C++ 编译器要求模板参数只能是数据类型——数据类型是永远都没有反作用的。与之相应,编译器须要实现模板形式参数的替换、函数重载、重复模板实例消除等功能。C++ 有点像大禹,挖沟开河,折腾了许多年,终于将宏这种难以驾驭的洪水猛兽在必定程度上控制住了。C++ 的模板比宏要安全一些,并且项目开发效率也提高了一个台阶。编程语言
C++ 模板本质上依然是宏。尽管模板的参数是类型,可是 C++ 编译器没法肯定对参数是否正确。一旦 C++ 模板参数出错,编译器就会愚蠢的给出一堆不知所云的错误信息。换句话说,从 C++ 编译器的角度看,模板的参数本质上只是文本,它没法对这种文本进行逻辑上的判断。发现这个问题的存在以后, C++ 社区又发展出一个新的概念,这个概念就叫作概念(Concept)。基于 Concept,能够对模板参数的『行为』进行约束。对于传递给模板的类型,C++ 编译器会检查这个类型是否拥有模板参数应该具备的行为——有点像动态类型语言里的鸭子类型(Duck Typing)。Concept 就是类型的类型。不知是何缘由,C++ 标准委员会三番五次的否决 Concept 的提案。函数式编程
Haskell提供了一种比模板更高级的代码复用形式。C++ 的模板参数,对于 Haskell 而言就是函数签名以及编译器的自动类型推导。C++ 社区求之不得的类型的类型,对于 Haskell 而言就是类型类(TypeClass)。也就是说,Haskell 已经将以数据类型为形参的宏的反作用完全的消除了。单从语言层面上来看,若是可以习惯 Haskell 不支持赋值运算这一特色,能够将 Haskell 视为更好的 C++。函数
这一切看上去都很美好,可是借助宏来扩展自身的语法,这种需求彷佛被大部分编程语言的设计者刻意的忽视了。可能他们认为,对语言自身进行扩展,那是语言标准委员会以及编译器开发者的任务,而不是软件开发者的任务。不少人认为,纵容软件开发者对语言进行扩展会形成语言的分裂。他们会说,不妨统计一下,这个世界上有多少个版本的 Lisp 与 Scheme 语言的实现。对宏进行弱化,能解决语言分裂的问题么?我以为这只是回避问题的办法,而不是解决问题的办法。能够想想,有些库自称是框架,它们所作的事情是否是企图基于类或高阶函数对语言自己进行扩展?若是可以很面向特定领域,为语言增长一些扩展,使之成为领域专用语言,这岂不是比框架要好得多得多?学习
宏的真正用武之地就在于操纵语言自身的语法,为某些特定的问题领域定制更易于理解与应用的语法。所谓的元编程,其用意大抵也是如此。
譬如 C 语言的宏,虽然其功能极弱——只能展开一次,可是依然能为 C 扩展出好用一些的语法。例如 GObject 库为基于 C 语言提供面向对象提供了有力支持,下面是它的一个空的类的定义示例:
typedef struct _MyObject{ GObject parent_instance; } MyObject; typedef struct _MyObjectClass { GObjectClass parent_class; } MyObjectClass; G_DEFINE_TYPE(MyObject, my_object, G_TYPE_OBJECT);
它等效于下面的 C++ 代码:
class MyObject { };
G_DEFINE_TYPE
能够将一个结构体类型注册到 GObject 实现的动态类型系统,从而产生一个相似于 C++ 的『类』的类型。
C 编译器展开 G_DEFINE_TYPE
宏后,大体能够获得如下 C 代码:
static void my_object_init(MyObject * self); static void my_object_class_init(MyObjectClass * klass); static gpointer my_object_parent_class = ((void *) 0); static gint MyObject_private_offset; static void my_object_class_intern_init(gpointer klass) { my_object_parent_class = g_type_class_peek_parent(klass); if (MyObject_private_offset != 0) g_type_class_adjust_private_offset(klass, &MyObject_private_offset); my_object_class_init((MyObjectClass *) klass); } __attribute__ ((__unused__)) static inline gpointer my_object_get_instance_private(const MyObject * self) { return (((gpointer) ((guint8 *) (self) + (glong) (MyObject_private_offset)))); } GType my_object_get_type(void) { static volatile gsize g_define_type_id__volatile = 0; if (g_once_init_enter(&g_define_type_id__volatile)) { GType g_define_type_id = g_type_register_static_simple(((GType) ((20) << (2))), g_intern_static_string("MyObject"), sizeof(MyObjectClass), (GClassInitFunc) my_object_class_intern_init, sizeof(MyObject), (GInstanceInitFunc) my_object_init, (GTypeFlags) 0); } return g_define_type_id__volatile; };
虽然使用 GObject 来编写面向对象的 C 程序要比 C++ 繁琐一些,可是学习成本却低了许多。若是 C 的宏可以像 m4 那样强,在语言层面基于宏精心扩展,在语言层面支持面向对象编程范式并不是难事。我曾经用 GNU m4 实现过一个简单的单层匿名函数机制,偶尔能够吓到一些人。
include(`c-closure.m4')dnl #include <stdio.h> _C_CORE int main(void) { char *str_array[5] = {"fetch", "foo", "foobar", "sin", "atan"}; @(`char *', `foo', `"foo"'); qsort(str_array, 5, sizeof(char *), _LAMBDA(`const void *p1, const void *p2', `int', `int d1 = dist(* (char * const *) p1, &(`foo')); int d2 = dist(* (char * const *) p2, &(`foo')); if (d1 < d2) { return -1; } else if (d1 == d2) { return 0; } else { return 1; }')); for (int i = 1; i < 5; i++) { printf("%s\n", str_array[i]); } exit(EXIT_SUCCESS); }
从如今开始,就应该牢记:宏是用来扩展语法的,不要将它做为函数来用。Scheme R5RS 标准中强调了这一点,而且将宏定义语法设置为如下格式:
(define-syntax macro <syntax transformer>)
下面这段代码为 Guile 定义了相似 C 语言的 if .. else if ... else
语法:
(define-syntax if' (syntax-rules () ((if' e result) (cond (e result))) ((if' e result else' <result>) (cond (e result) (else <result>))) ((if' e result else' if' <e> ...) (cond (e result) (else (if' <e> ...))))))
因为 Guile 有本身的 if
语法,因此我在 if
后面加了个单引号以示区别。
if'
宏的用法以下:
(if' #f (display "1") else' if' #f (display "2") else' if' #f (display "3") else' (display "nothing"))
因为我如今只是 Guile 的初学者,因此我并不保证 if'
的定义是否正确。为了写出这个宏,我大概折腾了半个下午。不过,这个宏的意图很简单,它可让咱们在编写条件分支时少写一些括号。
这个 if'
宏是由三条语法规则——syntax-rules
块中的三个表达式构成的。来看第一条语法规则:
((if' e result) (cond (e result)))
这条语法规则描述的是,若是遇到像 (if' e result)
这样的表达式,Guile 便将其转换为 (cond (e result)))
。表达式 (if' e result)
被称为模式,它表示的是含有三个元素的列表。也就是说,凡是含有三个元素的列表,都是 (if' e result)
模式,这样的列表有无数个,可是其中大部分不是咱们须要的。由于若是要使用 if'
宏,这个列表的第一个元素应该是 if'
符号,其他两个元素应该知足 cond
的要求。下面这些表达式都符合 (if' e result)
模式:
(if' #t (display "Hello world!")) (if' 2 3) (if' (< 2 3) #t) (if' (display "Hello") (display " world!"))
在上述表达式中,咱们使用 if'
宏,本质上是让 (if' e result)
这个模式与上述这些表达式进行匹配,这个过程被称为模式匹配。一旦模式匹配成功,Guile 会将模式中的各个符号便会与其匹配的子表达式绑定起来。在语法规则中,位于模式表达式右侧的那个表达式称为模板。每一个模式表达式对应着一个模板。模板经过模式中的符号来引用这些符号所绑定的子表达式。能够将模式理解为宏的名字及其参数的声明,将模板视为宏的定义。
Scheme 采用语法规则的方式来定义宏,好处是能够定义多个同名的宏。用面向对象的术语来讲,就是 Scheme 宏是多态的。if'
的其余两个版本是:
((if' e result else' <result>) (cond (e result) (else <result>))) ((if' e result else' if' <e> ...) (cond (e result) (else (if' <e> ...))))
须要注意,Scheme 宏是容许递归的。(if' e result else' if' <e> ...)
模式所对应的模板中含有 if'
的递归。由于有了这个递归,因此 if'
宏能够支持多条 else' if'
从句。
按照 if'
宏的第二条语法规则中的 (if' e result else' <result>)
模式,能够像下面这样使用 if'
:
(if' #f #f else' #t)
可是,下面这个表达式:
(if' #f #f i-am-guest-actor #t)
它也符合 (if' e result else' <result>)
模式,由于 i-am-guest-actor
会被绑定到 else'
符号,而 else'
符号在模板中并无用到,因此它绑定了什么是无所谓的。可是,咱们显然是但愿 else'
有意义。
针对此类问题,Scheme 为语法规则提供了关键字支持。只需将上一节给出的 if'
宏的定义修改成:
(define-syntax if' (syntax-rules (else' if') ((if' e result) (cond (e result))) ((if' e result else' <result>) (cond (e result) (else <result>))) ((if' e result else' if' <e> ...) (cond (e result) (else (if' <e> ...))))))
这样,模式中的 if'
与 else'
便都被设定为关键字。在使用 if'
宏时,若是再随便用别的符号来代替 else'
或 else' if'
,那么 Guile 便会报错,说找不到匹配模式。
let
let
是个颇有用的语法,它能够在函数内开辟一个包含一些变量与一个计算过程的『局部环境』。若是没有 let
,就只能经过函数的嵌套来作这件事,结果会致使代码有些扭曲。
假设存在一个数学函数(取自 SICP 1.3.2 节):
$$f(x,y)=x(1+xy)^2+y(1-y)+(1+xy)(1-y)$$
如今为它写编一个 Guile 函数。众所周知,Guile 的前缀表达式在表现复杂的代数运算式时会失于繁琐。例如:
(define (f x y) (+ (* x (* (+ 1 (* x y)) (+ 1 (* x y)))) (* y (- 1 y)) (* (+ 1 (* x y)) (- 1 y))))
这么多年,是哪些人没良心的吹嘘 Scheme 简单又优美呢?
若是将上述的数学函数写为 $f(x,y)=xa^2 + yb + ab$,其中 $a$ 与 $b$ 分别为 $(1+xy)$ 与 $(1-y)$,那么就能够将 Guile 代码简化为:
(define (f x y) (+ (* x (* a a)) (* y b) (* a b)))
可是不可避免的面临一个问题,在函数 f
内,如何制造两个局部变量 a
与 b
呢?能够像下面这样来作:
(define (f x y) ((lambda (a b) (+ (* x (* a a)) (* y b) (* a b))) (+ 1 (* x y)) (- 1 y)))
就是在函数 f
内部定义一个匿名函数,并应用它。在应用这个匿名函数时,Guile 会将其形参 a
与 b
便会分别与实参 (+ 1 (* x y))
与 (- 1 y)
绑定起来。
用上面这样的方式写代码,是否是世界观有些扭曲?不过,咱们能够将这种扭曲的代码用宏封装起来,造成 let
语法。事实上,在 Guile 中,let
自己就是用宏实现的语法:
(define-syntax let (syntax-rules () ((let ((name val) ...) body1 body2 ...) ((lambda (name ...) body1 body2 ...) val ...))))
有了 let
,就能够将上面那个函数写为:
(define (f x y) (let ((a (+ 1 (* x y))) (b (- 1 y))) (+ (* x (* a a)) (* y b) (* a b))))
下面是 C 的一个宏的定义:
#define SWAP(x, y, type) {type c = x; x = y; y = c;}
这个宏用于交换两个同类型变量的值,其用例以下:
int a = 3, b = 7; SWAP(a, b, int);
结果 a
的值会变为 7
,b
的值会变为 3
,也就是说 a
与 b
的值被 SWAP
宏交换了。
看上去,SWAP
这个宏没有什么问题,可是它有着一种匪夷所思的反作用。看下面这个例子:
int b = 3, c = 7; SWAP(b, c, int);
若是不去看 SWAP
的定义,咱们会想固然的认为 b
与 c
的值会被 SWAP
交换,但事实上两者的值不会被交换。由于 C 预处理器会将上述代码处理为:
int b = 3, c = 7; {int c = b; b = c; c = c;}
{ ... }
里的 c
是一个局部变量,对它进行任何修改都不会影响 { ... }
外部的 c
。
若是将 SWAP
的定义修改成
#define SWAP(x, y, type) {type _______c = x; x = y; y = ______c;}
这样能够大部分状况下能够避免宏定义内部的临时变量与宏调用环境中的变量重名所带来的问题。不过,无人能保证不会有人向 SWAP
宏传递一个名字是 _______c
的参数。
这个故事告诉咱们,在使用 C 的宏时,最好对其定义有所了解。
如今来看 Guile 版本的 SWAP
宏的定义及用例:
(define-syntax swap (syntax-rules () ((swap x y) (let ((c y)) (set! y x) (set! x c))))) (define b 2) (define c 9) (swap b c)
结果 b
与 c
的值互相交换了。
在 syntax
环境中所用的临时变量,Guile 会自动对临时变量进行重命名,而且保证这个名字从未在宏的定义以外使用过。单凭这一功能,Scheme 的宏就能够藐视其余语言的宏了。
Scheme 的宏之因此能像耍魔术同样的制造出一些有用的语法,缘由在于 Scheme 语法形式上的统一,这种形式就是所谓的 S-表达式。不管传递给宏的文本有多么复杂,只要它在形式上是 S-表达式,那么在宏的内部即可以对这种文本从新组织。
用编译原理中的术语来讲,Scheme 的宏能够将其接受的一棵语法树转换为其余形式的语法树。所谓语法树,是目前任何一种比汇编语言高级的编程语言经编译器/解释器做语法分析后所得结果的抽象表示。Scheme 代码自己就是语法树,Scheme 解释器无需再对其进行语法分析,在这个层面上,经过宏能够自由的对语法树进行从新组织。只有运行于语法分析树层面上的宏机制才能具备 Scheme 宏这样的能力。
C/C++ 所提供的宏机制,运行于编译或解释阶段以前的预处理阶段,它的工做只是遵循特定规则的文本替换,预处理器并不知道本身所处理的文本的逻辑结构。此类宏机制相似于外科手术中的器官移植,而基于 S-表达式的 Scheme 宏则相似于基因重组。
本章的导言中说『若是稍微考察一下汇编语言,不难发现,汇编语言的宏也具有与 Scheme 宏的类似的特性』。虽然汇编语言的宏机制本质上也是文本替换,可是汇编语言向机器语言的转换是不须要语法分析的,而是汇编指令向机器指令的直接映射。所以,汇编语言的宏也是在作『基因重组』的工做。
像 TeX 与 m4 这样的软件,它们提供的宏功能比 C/C++ 宏机制更强大,但本质上依然是遵循特定规则的文本替换。因为文本自身就是它们操做的对象,因此它们的宏本质上是将文本视为『基因』进行重组。从这个角度来看,能够说它们的宏也具有与 Scheme 宏类似的特性。例如,m4 经常使用的宏基于一组基本的宏构建而成。Autoconf 是基于 m4 构建的一种领域专用语言。TeX 不可胜数的宏包,每一个宏包都是一种领域专用语言。不过,得之于宏,也失之于宏,譬如 TeX 虽然极为擅长展自身语法,可是它在编程方面却很是贫弱。正是因为这个缘故,因此才会出现 LuaTeX 项目,该项目尝试将 Lua 解释器嵌入 TeX 宏处理器,从而以 Lua 语言补了 TeX 在编程方面的不足。
Haskell 为软件开发者留了一个能够访问其语法分析结果的接口,所以 Haskell 也具备相似于 Scheme 宏的能力,只不过 Haskell 没有在语法层面提供宏。Haskell 爱好者们认为,Haskell 提供了惰性计算、准引用以及 GHC 的 API,基于这些机制,Scheme 宏能作到的事,Haskell 也能作到。