宏扩展的执行逻辑初探(一)

宏是怎么扩展的呢?

比较流行的编程语言中,Java,Python都没有定义宏的功能。c语言中的宏限制比较大, 没有Lisp中的宏强大。以下提到的宏,指的是Lisp方言中的宏。

宏的求值可以分为两个步骤

  1. 扩展(expand)–编译期
  2. 求值(eval) –运行期

这看起来很简单,但是实际写macro的时候,就会有很多的困惑。这里的主要矛盾就是理解 以下两个过程,

  1. 在一个宏的内部调用另一个函数
  2. 在一个宏的内部调用另一个宏

不同的lisp方言有不同的实现。以下用Clojure来研究宏的用法。

宏内部调用函数(call function inside macro)

宏扩展阶段,就会调用函数进行求值。

1
2
3
4
5
(use 'clojure.walk)
(defmacro fool [x]
(+ x 1))

(macroexpand-all '(fool 2))

运行结果如下,可以看到宏扩展阶段已经调用+函数进行了求值。

1
2
#'user/fool
3

这里的+是一个函数。

宏内部调用宏(call macro inside macro)

继续上面的例子,我们定义一个加法宏,然后在fool的内部调用该宏。

1
2
3
4
5
6
7
8
9
(use 'clojure.walk)

(defmacro plus [x y]
(+ x y))

(defmacro fool [x]
(plus x 1))

(macroexpand-all '(fool 2))

运行这段代码会得到以下的错误信息。重点就是理解这里为什么会报错。

1
CompilerException java.lang.ClassCastException: clojure.lang.Symbol cannot be cast to java.lang.Number, compiling:(/tmp/form-init4487875798119154898.clj:8:3) 

这里报错说是Symbol无法转换为数字。可是我们明明输入的是2。

事实的过程如下,

  1. 扩展 (fool 2)
  2. 进入fool的body
  3. 扩展 (plus x 1) — 这里x是一个Symbol,而不是x-value
  4. 进入宏plus的body
  5. 对(+ x 1)求值 —到这里报错了,因为x是一个Symbol。

所以,如果直接在一个宏的内部调用另一个宏,很可能编译都无法通过。因为编译期会对宏进行扩展, 但不会绑定实参。

那么怎么正确的使用呢,这里引入了一个概念叫emit a macro call inside a macro

1
2
3
4
5
6
7
8
9
10
(use 'clojure.walk)

(defmacro plus [x y]
(+ x y))

(defmacro fool [x]
(list 'plus x 1))

(macroexpand-1 '(fool 2)) ;; (plus 2 1)
(macroexpand-all '(fool 2)) ;; 3

结果如下,只扩展一次,会得到(plus 2 1), 而完全扩展会得到3

1
2
3
4
5
=> nil
#'user/plus
#'user/fool
(plus 2 1)
3

具体过程如下,

  1. 扩展(fool 2)
  2. 进入fool的body
  3. 求值(list ‘plus x-value 1) – 这里x-value是2
  4. 得到(plus x-value 1) – 这里x-value是2
  5. 发现仍然是一个宏,继续扩展
  6. 进入plus的body
  7. 求值(+ x-value 1)为3 – 这里x-value是2

语法引用(Syntax Quote)

正是因为需要频繁的使用list来进行宏调用,因此发明了这样的语法糖。

1
2
3
4
5
6
7
8
9
10
(use 'clojure.walk)

(defmacro plus [x y]
(+ x y))

(defmacro fool [x]
`(plus ~x 1)) ;; <- 这里等价于 (list 'plus x 1))

(macroexpand-1 '(fool 2))
(macroexpand-all '(fool 2))

宏扩展的执行逻辑初探(一)
https://threelambda.com/2018/11/09/minilisp/
作者
Ming Yang
发布于
2018年11月9日
许可协议