Elisp 10:宏

上一章:编程

上一章实现了只定义了一个函数的库 newbie.el。事实上,这个函数能够不用定义成函数,定义成宏也能够,并且能让调用代码的执行效率微乎其微地更高一些。由于,调用函数,就像是去车站乘坐客车,而调用宏,犹如乘坐自家的私家车。这是一个不是很准确的比喻,因此它仅仅是个比喻。segmentfault

定义宏

先定义一个什么也干不了的宏,函数

(defmacro foo ())

在形式上,定义宏,彷佛跟定义函数差很少,只是 defun 换成了 defmacrocode

调用一个宏,也跟调用一个函数差很少,例如调用上述定义的什么也干不了的宏 foo;get

(foo)

对于这个宏调用,Elisp 的求值结果是 nil。为何是 nil 呢?由于 Elisp 解释器遇到宏调用语句,会用宏的定义替换它,此即宏的展开。上述 (foo) 语句会被替换为class

就是什么都没有。什么都没有,就是 nil效率

假若是让 foo 的定义有点什么,例如变量

(defmacro foo ()
  t)

那么宏调用语句的展开结果就是 t语法

宏也能够像函数那样拥有参数,例如程序

(defmacro foo (x)
  x)

宏调用 (foo "Hello world!") 的展开结果即是 "Hello world!"

像构造数据同样构造程序

宏的定义,展示的是 Lisp 语言的一个很重要的特性,在程序里能够像构造数据同样地构造程序。例如

(defmacro foo ()
  (list '+ 1 2 3))

Elisp 解释器会对宏定义里的表达式予以求值。上述宏定义里的 (list '+ 1 2 3),求值结果就是 (+ 1 2 3)。所以,宏调用语句 (foo) 会被 Elisp 解释器展开为 (+ 1 2 3),而后 Elisp 解释器会对宏的展开结果继续进行求值,所以 (foo) 的求值结果是 6。利用 Elisp 解释器对宏的定义和调用的处理机制,即可以在程序里像构造数据同样地构造程序。

因为 (list '+ 1 2 3)'(+ 1 2 3) 近乎等价,所以上述宏定义可简化为

(defmacro foo ()
  '(+ 1 2 3))

在宏的定义里使用引号构造程序要注意引号会屏蔽 Elisp 解释器对参数的处理。例如

(defmacro foo (x y z)
  '(+ x y z))

这个宏的定义是合法的,可是若像下面这样调用它

(foo 1 2 3)

并不会被展开为 (+ 1 2 3),而是会被展开为 (+ x y z)。由于 Elisp 在对宏定义求值时,认为宏定义里的 '(+ x y z) 只是一个字面意义上的列表,其中的 xyz 并不是宏的参数值。所以,在宏的定义里,须要清楚,哪些是字面上的数据,哪些是变量或函数调用。对于上例,须要用回 list,即

(defmacro foo (x y z)
  (list '+ x y z))

如此,(foo 1 2 3) 便会被展开为

(+ 1 2 3)

反引号

宏定义

(defmacro foo (x y z)
  (list '+ x y z))

(defmacro foo (x y z)
  `(+ ,x ,y ,z))

同义。

引号 ' 可让一个列表总体变成字面意义上的列表,而反引号(一般在键盘上与 ~ 位于同一键位)也可让一个列表变成字面意义上的列表,可是假若前面由 , 修饰的符号,例如宏的参数,Elisp 解释器便再也不将其视为字面意义上的符号了。

在反引号做用的列表里,,@ 可将一个列表里的元素提高到外层列表,例如

`(1 ,@(list 2 3) 4)

`(1 ,@'(2 3) 4)

以及

`(1 ,@`(2 3) 4)

的求值结果皆为 (1 2 3 4)

利用这些奇怪的符号,在宏定义里像构造构造程序会更为便捷。

print! 宏

如下代码定义的宏

(defmacro print! (x)
  `(progn
     (princ ,x)
     (princ "\n")))

可代替 newbie.el 里的 princ\',例如

(print! "Hello world!")

变量捕获

有些时候,须要在宏的定义里使用局部变量。例如

(defmacro bar (x y a)
  `(let (z)
     (if (< ,x ,y)
         (setq z ,x)
       (setq z ,y))
     (+ ,a z)))

这个宏可将其参数 xy 中较小者与 a 相加。例如

(bar 2 3 1)

求值结果为 3。

bar 的调用若是出如今一些巧合的环境里,例如

(let ((z 1))
  (bar 2 3 z))

求值结果为 4,而不是 3。之因此会出现这种不符合预期的结果,是由于上述宏调用语句被展开为

(let ((z 1))
  (let (z)
    (if (< 2 3)
        (setq z 2)
      (setq z 3))
    (+ z z)))

之因此会出现这样的展开结果,是由于 Elisp 解释器不会对宏参数进行求值,而是将其原样传入宏的定义,用它们去替换宏的参数。(bar 2 3 z) 的第三个参数是 z,Elisp 解释器将这个参数原样传入 bar 的定义后,后者的参数 a 就被换成了 z,可是 bar 的定义里有一个局部变量 z,在最后的 (+ z z) 表达式里,第一个 z 本应是我传给 bar 的参数,可是 Elisp 解释器在这种状况下,会认为它是 bar 的局部变量,因而,计算结果便不符合个人预期了。

卫生宏

能保证宏定义里的局部变量不与宏展开环境里外部变量产生混淆的宏,称为「卫生宏」。Elisp 的宏不卫生。同为 Lisp 方言的 Scheme 语言提供了卫生宏。近年来,新兴的 Rust 语言也支持卫生宏。不过,Elisp 能够利用体制外(Uninterned)的符号模拟卫生宏。

Elisp 解释器在对程序解释执行的过程当中,会维护一些存储着符号的表,这些符号要么是绑定了数据,要么是绑定了函数,要么是绑定了宏。出如今这些表里的符号,就是体制内的(Interned),没出如今这个表里的符号,就是体制外的。使用 Elisp 函数 make-symbol 能够建立体制外的符号。例如

(setq z 3)
(setq other-z (make-symbol "z"))

第一个表达式里的 z 是绑定到数字 3 的符号,它是体制内的,而 make-symbol 建立的符号也叫 z,但它是体制外的,我用一个体制内的符号 other-z 绑定了这个体制外的也叫 z 的符号。利用这个 other-z 绑定的体制外的 z 符号,即可以令上一节定义的宏 bar 变得卫生,即

(defmacro bar (x y a)
  (let ((other-z (make-symbol "z")))
    `(progn
       (if (< ,x ,y)
           (setq ,other-z ,x)
         (setq ,other-z ,y))
       (+ ,a ,other-z))))

bar 的新定义不再怕变量捕捉了。试试看,

(let ((other-z 1))
  (bar 2 3 other-z))

在上述调用 bar 的语句里,虽然第三个参数与 bar 定义里的局部变量 other-z 同名,可是不会再发生变量捕捉的状况了,于是上述代码的求值结果为 3。

从新定义的 bar 是如何避免变量捕捉的呢?要理解这一切,就要对 Elisp 如何对宏的定义进行求值有深入的理解。首先,Elisp 解释器会对宏定义里的任何一个表达式进行求值,假若想禁止它对某个表达式求值,那就须要用引号。用引号修饰的表达式,Elisp 解释器会将其视为常量。可是,经过反引号以及逗号,能够在 Elisp 视为常量的表达式里开辟一些可变之处,后者即是从新定义的 bar 能避免变量捕捉的关键,由于 Elisp 对宏定义的常量部分不会求值,可是常量里可变的地方会进行求值。这就至关于,在宏定义里,可让一段代码处于「静止」的状态,而让这段代码里的部分区域是能够被 Elisp 解释器修改为咱们须要的结果。

bar 的定义里会本来会发生变量捕捉的语句是

(+ ,a ,other-z)

因为 other-z 已是在 let 表达式的开头将其绑定到一个体制外的符号 z 了,因此 Elisp 解释器在对宏定义求值时,会认为全部的 ,other-z 视为(或求值为)这个体制外的符号 z,亦即等 bar 调用语句被 Elisp 展开后,符号 other-z 已经不是 other-z 了,而是那个体制外的 z。在 bar 的定义里,做为局部变量的 other-z 绝无可能再与外部同名的变量产生混淆了。这就是 Elisp 语言构造卫生宏的办法。

事实上,在上述 bar 的定义里,我根本不必使用 other-z,彻底能够像下面这样定义 bar

(defmacro bar (x y a)
  (let ((z (make-symbol "z")))
    `(progn
       (if (< ,x ,y)
           (setq ,z ,x)
         (setq ,z ,y))
       (+ ,a ,z))))

在上述代码的 let 表达式里,体制内的符号 z 绑定到体制外的符号 z,而后在后续的代码里,,z 皆会被 Elisp 解释器求值为体制外的符号 z,如此一来,如下宏调用语句

(let ((z 1))
  (bar 2 3 z))

求值结果符合预期,为 3。

体制外的,有助于卫生建设。

结语

本章仅介绍了 Elisp 宏最为浅显的知识,它真正的用武之地是为 Elisp 语言定义新的语法(这种方式一般称为元编程),而非定义 print! 这种本来就能够用函数轻易实现的东西。

相关文章
相关标签/搜索