所有编程语言的语法设计都有终止的一天, 有了宏系统,语言设计人员可以不停的通过宏系统扩展新语法, 使得编程语言永葆青春,不受语言语法限制。
世事皆有两面性,得失之间相互依存。 宏系统可以极大减少单调冗繁的代码,构造出强大的领域特定语言(DSL, Domain Specific Language), 但是宏的展开比函数调用更难于调试,使用宏需要极其细心,同时也使代码更难于理解。
正是由于上述原因,上世纪八九十年代及世纪之交, 新出现的编程语言都极力避免使用宏系统, 大多利用语言自省功能来进行元编程, 但最近几年,有些静态语言(rust、crystal、julia、...)又开始引入了宏的高级功能。
上篇博客中我们了解到 C 语言宏只是词法 token 级别的简单替换, 接下来我们通过 lisp 语言和 scheme 来讲解较高级的宏系统, 语法级别的用户自定义替换。
理解本文需要你事先熟悉 lisp 语法,以及 sexp 表达式。
lisp 中的宏
lisp 语言有 if
条件基本控制语句,而 unless
条件控制结构就是一个宏,
下面看看 lisp 中如何定义一个 unless
宏:
(defmacro unless (condition body)
(list 'if ('not condition) body))
lisp 为了简化 macro 的定义,引入了 quasiquote
、unquote
、unquote-splicing
简写形式,
下面是用 quasiquote
来定义同样的 unless
宏的方法:
(defmacro unless (condition body)
`(if (not ,condition) ,body))
上面两种写法都是定义(defmacro, define macro)一个叫 unless
的宏,
这样,代码中如果遇到 (unless some-condition body)
形式,
都会先将该形式的语法转换成 (if (not some-condition) body)
的形式,
然后由 lisp 解释执行。
上面我们可以看到,lisp 定义宏,类似于定义一个函数, 只是这个函数的参数是宏的语法输入,而函数返回的值就是宏的展开, 而又因为 lisp 的代码和数据具有同构性(这也是 sexp 表达式强大之处), 使得宏的展开定义过程极其容易,这也是 lisp 中宏得到普及的一个原因。
不卫生宏
虽然 lisp 中定义宏极其方便简单,
但是使用它却要极其小心,因为它有些缺点,就是不卫生
,
也就是说 lisp 中宏的展开极易受到运行时上下文环境的影响,
产生意向不到的结果,以至于报错。
比如来看下面 swap
宏的定义:
(defmacro swap (a, b)
`(let ((tmp ,a))
(set! ,a ,b)
(set! ,b tmp)))
使用上面的宏,我们会碰到两种类型的错误:
- 宏定义引入了重名的符号绑定问题
- 宏定义使用了运行环境中被重新绑定的符号引用问题
宏定义引入了重名的符号绑定问题
使用 swap 宏时,如果其参数与 swap 定义时引入的 tmp
变量重名,
就会导致相关变量作用域被改变的问题:
(define tmp 1)
(define y 2)
(swap tmp y)
上面的 (swap tmp y)
在宏展开后就成了:
(let ((tmp tmp))
(set! tmp y)
(set! y tmp))
这样就会使宏不能达到预期效果。
宏定义使用了运行环境中被重新绑定的符号引用问题
我们在定义宏时用了 set!
绑定,
如果我们在使用宏时,重新绑定了 set!
符号,也会使宏不能达到预期效果,
如下:
(define x 1)
(define y 2)
(let ((set! display))
(swap x y)
如何使用 lisp 宏
为了解决上面第 1 个问题,lisp 中引入了 gensym
函数,调用它会生成一个独一无二的符号,
这样就可以保证绝对不会出现重名问题:
(defmacro swap (a, b)
(let ((tmp (gensym))
`(let ((,tmp ,a))
(set! ,a ,b)
(set! ,b ,tmp)))))
这样在展开 (swap tmp y)
时,就会展开成:
(let ((<tmp-uniq-symbol> tmp))
(set! tmp y)
(set! y <tmp-uniq-symbol>))
但是,lisp 中对于第2种问题却没有什么较好的解决办法, 只能让程序员小心定义和使用宏,避免出现这种问题。
scheme 中的卫生宏
为了彻底解决 lisp 上面出现的两种问题,lisp 语言的一个分支 scheme 引入了卫生宏
的概念。
用 scheme 中宏定义的方法来定义 swap 宏如下:
(define-syntax swap
(syntax-rules ()
((swap a b) (let ((tmp a))
(set! a b)
(set! b tmp)))))
scheme 中的 syntax-rules
中尽管引入了 tmp 和 set! 符号,
但是 scheme 实现却保证了:
tmp
在宏扩展时会被重命名,保证唯一性set!
在宏扩展时绑定的对象是宏定义时环境中的set!
对象
这样通过引入 syntax-rules
宏定义,scheme 语言彻底解决了不卫生宏的问题。