Fu
Simple is Beautiful!

编程语言宏之进阶

所有编程语言的语法设计都有终止的一天, 有了宏系统,语言设计人员可以不停的通过宏系统扩展新语法, 使得编程语言永葆青春,不受语言语法限制。

世事皆有两面性,得失之间相互依存。 宏系统可以极大减少单调冗繁的代码,构造出强大的领域特定语言(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 的定义,引入了 quasiquoteunquoteunquote-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)))

使用上面的宏,我们会碰到两种类型的错误:

  1. 宏定义引入了重名的符号绑定问题
  2. 宏定义使用了运行环境中被重新绑定的符号引用问题

宏定义引入了重名的符号绑定问题

使用 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 实现却保证了:

这样通过引入 syntax-rules 宏定义,scheme 语言彻底解决了不卫生宏的问题。

macro3lisp3scheme30编译原理6
2022-06-06 11:45:00