乐趣区

关于lisp:Elisp-10宏

上一章:库

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

定义宏

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

(defmacro foo ())

在模式上,定义宏,仿佛跟定义函数差不多,只是 defun 换成了 defmacro

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

(foo)

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

就是什么都没有。什么都没有,就是 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! 这种本来就能够用函数轻易实现的货色。

退出移动版