1 Star 0 Fork 3

rookieagle/Elisp

forked from advanceflow/Elisp 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
14-宏.org 17.64 KB
一键复制 编辑 原始数据 按行查看 历史
advanceflow 提交于 2022-05-28 10:12 . finish s2

14 宏

宏使您能够定义新的控件结构和其他语言功能。 宏的定义很像函数,但它不是告诉如何计算一个值,而是告诉如何计算另一个 Lisp 表达式,该表达式将依次计算该值。 我们称这个表达式为宏的扩展。

宏可以这样做,因为它们对参数的未计算表达式进行操作,而不是像函数那样对参数值进行操作。 因此,它们可以构造一个包含这些参数表达式或其部分的扩展。

如果您使用宏来执行普通函数可以执行的操作,为了速度,请考虑使用内联函数。 请参阅内联函数。

14.1 一个简单的宏例子

假设我们想定义一个 Lisp 结构来增加一个变量值,就像 C 中的 ++ 运算符一样。我们想写 (inc x) 并具有 (setq x (1+ x)) 的效果。 这是一个完成这项工作的宏定义:

(defmacro inc (var)
   (list 'setq var (list '1+ var)))

当使用 (inc x) 调用 this 时,参数 var 是符号 x——而不是 x 的值,就像在函数中那样。 宏的主体使用它来构造展开式,即 (setq x (1+ x))。 一旦宏定义返回这个展开式,Lisp 就会继续计算它,从而增加 x。

Function: macrop object ¶

该谓词测试其参数是否为宏,如果是则返回 t,否则返回 nil。

14.2 宏调用的扩展

宏调用看起来就像函数调用,因为它是一个以宏名称开头的列表。 列表的其余元素是宏的参数。

宏调用的求值就像函数调用的求值一样开始,除了一个关键的区别:宏参数是出现在宏调用中的实际表达式。 在将它们提供给宏定义之前,它们不会被评估。 相比之下,函数的参数是评估函数调用列表元素的结果。

获得参数后,Lisp 调用宏定义就像调用函数一样。 宏的参数变量绑定到来自宏调用的参数值,或者在 &rest 参数的情况下绑定到它们的列表。 宏体像函数体一样执行并返回其值。

宏和函数之间的第二个关键区别是宏体返回的值是一个替代的 Lisp 表达式,也称为宏的扩展。 Lisp 解释器在扩展从宏返回后立即对其进行评估。

由于扩展是以正常方式评估的,它可能包含对其他宏的调用。 它甚至可能是对同一个宏的调用,尽管这是不寻常的。

请注意,Emacs 在加载未编译的 Lisp 文件时会尝试扩展宏。 这并不总是可能的,但如果是的话,它会加快后续执行的速度。 请参阅程序如何加载。

您可以通过调用 macroexpand 查看给定宏调用的扩展。

Function: macroexpand form &optional environment ¶

如果它是宏调用,则此函数扩展形式。 如果结果是另一个宏调用,则依次展开,直到结果不是宏调用。 那是宏扩展返回的值。 如果 form 开始时不是宏调用,则按给定返回。

请注意,macroexpand 不查看 form 的子表达式(尽管某些宏定义可能会这样做)。 即使它们本身是宏调用,macroexpand 也不会扩展它们。

函数 macroexpand 不会扩展对内联函数的调用。 通常不需要这样做,因为调用内联函数并不比调用普通函数更难理解。

如果提供了环境,它指定了一个隐藏当前定义的宏的宏定义列表。 字节编译使用此功能。



   (defmacro inc (var)
	   (list 'setq var (list '1+ var)))


   (macroexpand '(inc r))
	    ⇒ (setq r (1+ r))


   (defmacro inc2 (var1 var2)
	   (list 'progn (list 'inc var1) (list 'inc var2)))


   (macroexpand '(inc2 r s))
	    ⇒ (progn (inc r) (inc s))  ; inc not expanded here.

Function: macroexpand-all form &optional environment ¶

macroexpand-all 扩展类似macroexpand 的宏,但会查找并扩展所有形式的宏,而不仅仅是在顶层。 如果没有扩展宏,则返回值为 eq 以形成。

用 macroexpand-all 重复上面用于 macroexpand 的示例,我们看到 macroexpand-all 确实扩展了对 inc 的嵌入式调用:

  (macroexpand-all '(inc2 r s))
	    ⇒ (progn (setq r (1+ r)) (setq s (1+ s)))
Function: macroexpand-1 form &optional environment ¶

这个函数像macroexpand一样扩展宏,但它只执行一个扩展步骤:如果结果是另一个宏调用,macroexpand-1不会扩展它。

14.3 宏和字节编译

你可能会问我们为什么要麻烦计算宏的展开然后评估展开。 为什么不让宏体直接产生想要的结果呢? 原因与编译有关。

当宏调用出现在正在编译的 Lisp 程序中时,Lisp 编译器会像解释器一样调用宏定义,并接收扩展。 但它不是评估这个扩展,而是编译这个扩展,就好像它直接出现在程序中一样。 因此,编译后的代码会产生预期用于宏的值和副作用,但会以全编译速度执行。 如果宏体自己计算值和副作用,这将不起作用——它们将在编译时计算,这是没有用的。

为了使宏调用的编译工作,宏必须在编译对它们的调用时已经在 Lisp 中定义。 编译器有一个特殊功能可以帮助您做到这一点:如果正在编译的文件包含 defmacro 形式,则该宏将临时定义用于该文件的其余编译。

对文件进行字节编译还会在文件的顶层执行任何 require 调用,因此您可以通过要求定义宏定义的文件来确保在编译期间必要的宏定义可用(请参阅功能)。 为了避免在有人运行已编译的程序时加载宏定义文件,请在 require 调用周围编写 eval-when-compile(请参阅编译期间的评估)。

14.4 定义宏

Lisp 宏对象是一个列表,其 CAR 为宏,其 CDR 为函数。 宏的扩展通过将函数(使用 apply)应用于宏调用中未计算的参数列表来实现。

可以像使用匿名函数一样使用匿名 Lisp 宏,但这永远不会这样做,因为将匿名宏传递给诸如 mapcar 之类的函数是没有意义的。 在实践中,所有的 Lisp 宏都有名字,而且它们几乎总是用 defmacro 宏定义的。

Macro: defmacro name args [doc] [declare] body… ¶

defmacro 将符号名称(不应被引用)定义为如下所示的宏:

(macro lambda args . body)

(注意这个列表的 CDR 是一个 lambda 表达式。)这个宏对象存储在 name 的函数单元格中。 args 的含义与函数中的含义相同,可以使用关键字 &rest 和 &optional(参见参数列表的特性)。 name 和 args 都不应该被引用。 defmacro 的返回值是未定义的。

doc,如果存在的话,应该是一个指定宏的文档字符串的字符串。 如果存在,则声明应该是为宏指定元数据的声明表单(请参阅声明表单)。 请注意,宏不能有交互式声明,因为它们不能被交互式调用。

宏通常需要从常量和非常量部分的混合中构建大型列表结构。 为了使这更容易,请使用 ‘`’ 语法(请参阅反引号)。 例如:

(defmacro t-becomes-nil (variable)
  `(if (eq ,variable t)
	 (setq ,variable nil)))


(t-becomes-nil foo)
     ≡ (if (eq foo t) (setq foo nil))

14.5 使用宏的常见问题

宏观扩张可能会产生违反直觉的后果。 本节描述了一些可能导致麻烦的重要后果,以及避免麻烦的规则。

14.5.1 错误时间

编写宏时最常见的问题是过早地做一些实际工作——在扩展宏时,而不是在扩展本身中。 例如,一个真正的包有这个宏定义:

(defmacro my-set-buffer-multibyte (arg)
  (if (fboundp 'set-buffer-multibyte)
      (set-buffer-multibyte arg)))

使用这个错误的宏定义,程序在解释时工作正常,但在编译时失败。 这个宏定义在编译时调用了set-buffer-multibyte,这是错误的,然后编译的包运行时什么都不做。 程序员真正想要的定义是这样的:

 (defmacro my-set-buffer-multibyte (arg)
   (if (fboundp 'set-buffer-multibyte)
	`(set-buffer-multibyte ,arg)))

如果合适,此宏扩展为对 set-buffer-multibyte 的调用,该调用将在编译的程序实际运行时执行。

14.5.2 反复评估宏参数

定义宏时,您必须注意执行扩展时将评估参数的次数。 下面的宏(用于促进迭代)说明了这个问题。 这个宏允许我们编写一个 for 循环结构。



(defmacro for (var from init to final do &rest body)
  "Execute a simple \"for\" loop.
For example, (for i from 1 to 10 do (print i))."
  (list 'let (list (list var init))
	(cons 'while
	      (cons (list '<= var final)
		    (append body (list (list 'inc var)))))))


(for i from 1 to 3 do
   (setq square (* i i))
   (princ (format "\n%d %d" i square)))
→

(let ((i 1))
  (while (<= i 3)
    (setq square (* i i))
    (princ (format "\n%d %d" i square))
    (inc i)))


     -|1       1
     -|2       4
     -|3       9
⇒ nil

这个宏中的参数 from、to 和 do 是语法糖; 他们完全被忽略了。 这个想法是您将在宏调用中的这些位置写入干扰词(例如 from、to 和 do)。

这是通过使用反引号简化的等效定义:

(defmacro for (var from init to final do &rest body)
  "Execute a simple \"for\" loop.
For example, (for i from 1 to 10 do (print i))."
  `(let ((,var ,init))
     (while (<= ,var ,final)
	 ,@body
	 (inc ,var))))

此定义的两种形式(带反引号和不带反引号)都存在每次迭代都会评估 final 的缺陷。 如果 final 是一个常数,这不是问题。 如果它是更复杂的形式,例如(long-complex-calculation x),这会显着减慢执行速度。 如果 final 有副作用,多次执行它可能是不正确的。

一个设计良好的宏定义会采取措施来避免这个问题,方法是生成一个只对参数表达式求值一次的扩展,除非重复求值是宏的预期目的的一部分。 这是 for 宏的正确扩展:

 (let ((i 1)
	(max 3))
   (while (<= i max)
     (setq square (* i i))
     (princ (format "%d      %d" i square))
     (inc i)))

这是创建此扩展的宏定义:

(defmacro for (var from init to final do &rest body)
  "Execute a simple for loop: (for i from 1 to 10 do (print i))."
  `(let ((,var ,init)
	   (max ,final))
     (while (<= ,var max)
	 ,@body
	 (inc ,var))))

不幸的是,此修复引入了另一个问题,将在下一节中描述。

14.5.3 宏展开中的局部变量

在上一节中,for 的定义被固定如下,以使扩展评估宏参数的正确次数:



(defmacro for (var from init to final do &rest body)
  "Execute a simple for loop: (for i from 1 to 10 do (print i))."

  `(let ((,var ,init)
	 (max ,final))
     (while (<= ,var max)
       ,@body
       (inc ,var))))

for 的新定义有一个新问题:它引入了一个名为 max 的局部变量,这是用户不希望的。 这会导致以下示例中的问题:

(let ((max 0))
  (for x from 0 to 10 do
    (let ((this (frob x)))
      (if (< max this)
	  (setq max this)))))

for 主体内对 max 的引用,应该是指用户对 max 的绑定,实际上访问了 for 的绑定。

更正此问题的方法是使用非实习符号而不是 max(请参阅创建和实习符号)。 uninterned 符号可以像任何其他符号一样被绑定和引用,但是由于它是由 for 创建的,因此我们知道它不可能已经出现在用户的程序中。 由于它没有被实习,因此用户以后无法将其放入程序中。 它永远不会出现在任何地方,除非放在 for 的地方。 以下是这样工作的 for 的定义:

(defmacro for (var from init to final do &rest body)
  "Execute a simple for loop: (for i from 1 to 10 do (print i))."
  (let ((tempvar (make-symbol "max")))
    `(let ((,var ,init)
	     (,tempvar ,final))
	 (while (<= ,var ,tempvar)
	   ,@body
	   (inc ,var)))))

这将创建一个名为 max 的非驻留符号并将其放在展开式中,而不是通常出现在表达式中的常用驻留符号 max。

14.5.4 评估扩展中的宏观参数

如果宏定义本身评估任何宏参数表达式,例如通过调用 eval(请参阅 Eval),则可能会出现另一个问题。 您必须考虑到宏扩展可能在代码执行之前很久就发生了,此时调用者的上下文(将评估宏扩展)还无法访问。

此外,如果您的宏定义不使用词法绑定,则其形式参数可能会隐藏用户的同名变量。 在宏体内,宏参数绑定是此类变量的最局部绑定,因此正在评估的表单内的任何引用都会引用它。 这是一个例子:

(defmacro foo (a)
  (list 'setq (eval a) t))

(setq x 'b)
(foo x) → (setq b t)
     ⇒ t                  ; and b has been set.
;; but
(setq a 'c)
(foo a) → (setq a t)
     ⇒ t                  ; but this set a, not c.

用户变量命名为 a 还是 x 会有所不同,因为 a 与宏参数变量 a 冲突。

此外,上面的 (foo x) 的扩展将在编译代码时返回不同的东西或发出错误信号,因为在这种情况下 (foo x) 在编译期间被扩展,而 (setq x ‘b) 的执行将只需要在代码执行后放置。

为避免这些问题,在计算宏展开时不要计算参数表达式。 相反,将表达式替换为宏扩展,以便其值将作为执行扩展的一部分进行计算。 这就是本章中其他示例的工作方式。

14.5.5 宏扩展了多少次?

有时会出现问题,因为宏调用每次在解释函数中求值时都会扩展,但对于编译函数仅扩展一次(在编译期间)。 如果宏定义有副作用,它们的工作方式会有所不同,具体取决于宏扩展的次数。

因此,除非您真的知道自己在做什么,否则您应该避免计算宏展开时的副作用。

无法避免一种特殊的副作用:构造 Lisp 对象。 几乎所有的宏扩展都包含构造列表; 这是大多数宏的重点。 这通常是安全的; 只有一种情况你必须小心:当你构造的对象是宏扩展中带引号的常量的一部分时。

如果宏在编译过程中只展开一次,那么对象在编译过程中只被构造一次。 但是在解释执行中,每次宏调用运行时都会扩展宏,这意味着每次都会构造一个新对象。

在大多数干净的 Lisp 代码中,这种差异并不重要。 仅当您对宏定义构造的对象执行副作用时才有意义。 因此,为避免麻烦,请避免对由宏定义构造的对象产生副作用。 以下是此类副作用如何让您陷入困境的示例:

(defmacro empty-object ()
  (list 'quote (cons nil nil)))


(defun initialize (condition)
  (let ((object (empty-object)))
    (if condition
	  (setcar object condition))
    object))

如果 initialize 被解释,则每次调用 initialize 时都会构造一个新列表 (nil)。 因此,调用之间没有副作用。 如果 initialize 被编译,那么宏空对象在编译期间被扩展,产生一个常量(nil),每次调用 initialize 时都会重用和更改。

避免这种病态情况的一种方法是将空对象视为一种有趣的常量,而不是内存分配结构。 您不会在诸如 ‘(nil) 之类的常量上使用 setcar,因此自然也不会在 (empty-object) 上使用它。

14.6 缩进宏

在宏定义中,您可以使用声明形式(请参阅定义宏)来指定 TAB 应如何缩进对宏的调用。 缩进规范是这样写的:

(declare (indent indent-spec))

这导致在宏名称上设置 lisp-indent-function 属性。

以下是缩进规范的可能性:

nil

这与无属性相同——使用标准缩进模式。

defun

像处理“def”结构一样处理这个函数:将第二行视为正文的开始。

an integer, number

函数的第一个参数是区分参数; 其余的被认为是表达式的主体。 表达式中的一行根据其上的第一个参数是否被区分而缩进。 如果参数是正文的一部分,则该行缩进 lisp-body-indent 列比开始包含表达式的左括号多。 如果参数被区分并且是第一个或第二个参数,则缩进两倍的额外列。 如果参数被区分而不是第一个或第二个参数,则该行使用标准模式。

a symbol, symbol

symbol 应该是函数名; 调用该函数来计算此表达式中行的缩进。 该函数接收两个参数:

pos

缩进行开始的位置。

state

parse-partial-sexp(一种用于缩进和嵌套计算的 Lisp 原语)在解析到此行开头时返回的值。

它应该返回一个数字,即该行的缩进列数,或者一个列表,其 car 是这样的数字。 返回数字和返回列表的区别在于,数字表示同一嵌套级别的所有后续行都应该像这个一样缩进; 一个列表说以下几行可能需要不同的缩进。 当缩进由 CMq 计算时,这会有所不同; 如果该值是一个数字,CMq 不需要重新计算以下行的缩进,直到列表末尾。

马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/rookieagle/elisp.git
git@gitee.com:rookieagle/elisp.git
rookieagle
elisp
Elisp
main

搜索帮助