2 Star 13 Fork 3

advanceflow/Elisp

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

13 函数

Lisp 程序主要由 Lisp 函数组成。 本章解释什么是函数,它们如何接受参数,以及如何定义它们。

13.1 什么是函数?

在一般意义上,函数是在给定输入值(称为参数)的情况下执行计算的规则。 计算的结果称为函数的值或返回值。 计算也可能有副作用,例如变量值或数据结构内容的持久变化(请参阅副作用的定义)。 纯函数是一个函数,除了没有副作用之外,它总是为相同的参数组合返回相同的值,而不管外部因素如机器类型或系统状态如何。

在大多数计算机语言中,每个函数都有一个名称。 但是在 Lisp 中,最严格意义上的函数没有名称:它是一个对象,可以选择与用作函数名称的符号(例如,汽车)相关联。 请参阅命名函数。 当一个函数被命名时,我们通常也将该符号称为“函数”(例如,我们称为“函数汽车”)。 在本手册中,函数名称和函数对象本身之间的区别通常并不重要,但我们会注意任何相关的地方。

某些类似函数的对象,称为特殊形式和宏,也接受参数来执行计算。 但是,如下所述,这些在 Emacs Lisp 中不被视为函数。

以下是函数和类函数对象的重要术语:

lambda expression

用 Lisp 编写的函数(严格意义上,即函数对象)。 这些将在下一节中描述。 请参阅 Lambda 表达式。

primitive ¶

一个可从 Lisp 调用但实际上是用 C 编写的函数。基元也称为内置函数或 subrs。 示例包括 car 和 append 等函数。 此外,所有特殊形式(见下文)也被视为原语。

通常,函数被实现为原语,因为它是 Lisp 的基本部分(例如,汽车),或者因为它为操作系统服务提供低级接口,或者因为它需要快速运行。 与 Lisp 中定义的函数不同,只能通过更改 C 源代码和重新编译 Emacs 来修改或添加原语。 请参阅编写 Emacs 基元。

special form

类似于函数但不以通常方式评估其所有参数的原语。 它可能只评估一些参数,或者可能以不寻常的顺序或多次评估它们。 示例包括 if、and 和 while。 见特殊表格。

macro

Lisp 中定义的构造,它与函数的不同之处在于它将 Lisp 表达式转换为另一个要计算的表达式,而不是原始表达式。 宏使 Lisp 程序员可以做特殊形式可以做的事情。 请参阅宏。

command ¶

可以通过命令执行原语调用的对象,通常是由于用户键入绑定到该命令的键序列。 请参阅交互式呼叫。 一个命令通常是一个函数; 如果函数是用 Lisp 编写的,则在函数定义中通过交互形式将其制成命令(参见定义命令)。 作为函数的命令也可以从 Lisp 表达式中调用,就像其他函数一样。

键盘宏(字符串和向量)也是命令,即使它们不是函数。 请参阅键盘宏。 如果一个符号的功能单元包含一个命令,我们就说它是一个命令(参见符号组件); 可以使用 Mx 调用这样的命名命令。

closure

一个与 lambda 表达式非常相似的函数对象,不同之处在于它还包含了一个词法变量绑定的环境。 请参阅闭包。

byte-code function

已由字节编译器编译的函数。 请参阅字节码函数类型。

autoload object ¶

实际功能的占位符。 如果调用了自动加载对象,Emacs 会加载包含真实函数定义的文件,然后调用真实函数。 请参阅自动加载。

您可以使用函数 functionp 来测试一个对象是否是一个函数:

Function: functionp object ¶

如果 object 是任何类型的函数,即可以传递给 funcall,则此函数返回 t。 请注意,对于作为函数名称的符号,functionp 返回 t,对于特殊形式返回 nil。

还可以找出任意函数需要多少个参数:

Function: func-arity function ¶

此函数提供有关指定函数的参数列表的信息。 返回值是 (min . max) 形式的 cons 单元格,其中 min 是参数的最小数量,max 是参数的最大数量,或者是带有 &rest 参数的函数的符号 many,或者符号 unevalled if函数是一种特殊形式。

请注意,此函数在某些情况下可能会返回不准确的结果,例如:

  • 使用部分应用定义的函数(参见部分应用)。
  • 使用建议添加建议的函数(请参阅建议命名函数)。
  • 动态确定参数列表的函数,作为其代码的一部分。

与 functionp 不同,接下来的三个函数不将符号视为其函数定义。

Function: subrp object ¶

如果 object 是内置函数(即 Lisp 原语),则此函数返回 t。

  (subrp 'message)            ; message is a symbol,
	   ⇒ nil                 ;   not a subr object.

  (subrp (symbol-function 'message))
	   ⇒ t
Function: byte-code-function-p object ¶

如果 object 是字节码函数,则此函数返回 t。 例如:

  (byte-code-function-p (symbol-function 'next-line))
	   ⇒ t
Function: subr-arity subr ¶

这类似于 func-arity,但仅适用于内置函数并且没有符号间接。 它表示非内置函数的错误。 我们建议改用 func-arity。

13.2 Lambda 表达式

lambda 表达式是用 Lisp 编写的函数对象。 这是一个例子:

(lambda (x)
  "Return the hyperbolic cosine of X."
  (* 0.5 (+ (exp x) (exp (- x)))))

在 Emacs Lisp 中,这样的列表是一个有效的表达式,它的计算结果是一个函数对象。

lambda 表达式本身没有名称。 它是一个匿名函数。 尽管可以以这种方式使用 lambda 表达式(请参阅匿名函数),但它们更常与符号相关联以生成命名函数(请参阅命名函数)。 在进入这些细节之前,以下小节将描述 lambda 表达式的组件及其作用。

13.2.1 Lambda 表达式的组成部分

lambda 表达式是一个如下所示的列表:

(lambda (arg-variables…)
  [documentation-string]
  [interactive-declaration]
  body-forms…)

lambda 表达式的第一个元素始终是符号 lambda。 这表明该列表表示一个函数。 将函数定义为以 lambda 开头的原因是,其他用于其他用途的列表不会意外地作为函数有效。

第二个元素是符号列表——参数变量名称(参见参数列表的特性)。 这称为 lambda 列表。 当调用 Lisp 函数时,参数值将与 lambda 列表中的变量进行匹配,这些变量具有提供的值的本地绑定。 请参阅局部变量。

文档字符串是放置在函数定义中的 Lisp 字符串对象,用于描述 Emacs 帮助工具的函数。 请参阅函数的文档字符串。

交互式声明是一个表单列表(交互式代码字符串)。 如果以交互方式使用函数,这声明了如何提供参数。 具有此声明的函数称为命令; 它们可以使用 Mx 调用或绑定到一个键。 不打算以这种方式调用的函数不应具有交互式声明。 请参阅定义命令,了解如何编写交互式声明。

其余元素是函数的主体:完成函数工作的 Lisp 代码(或者,正如 Lisp 程序员所说,“要评估的 Lisp 表单列表”)。 函数返回的值是body最后一个元素的返回值。

13.2.2 一个简单的 Lambda 表达式示例

考虑以下示例:

(lambda (a b c) (+ a b c))

我们可以通过将其传递给 funcall 来调用此函数,如下所示:

(funcall (lambda (a b c) (+ a b c))
	   1 2 3)

此调用计算 lambda 表达式的主体,其中变量 a 绑定到 1,b 绑定到 2,c 绑定到 3。对主体的求值将这三个数字相加,产生结果 6; 因此,此函数调用返回值 6。

请注意,参数可以是其他函数调用的结果,如下例所示:

(funcall (lambda (a b c) (+ a b c))
	   1 (* 2 3) (- 5 4))

这将从左到右评估参数 1、(* 2 3) 和 (- 5 4)。 然后它将 lambda 表达式应用于参数值 1、6 和 1 以产生值 8。

正如这些示例所示,您可以使用带有 lambda 表达式作为其 CAR 的表单来创建局部变量并为其赋值。 在过去的 Lisp 时代,这种技术是绑定和初始化局部变量的唯一方法。 但如今,为此目的使用特殊形式 let 更加清晰(参见局部变量)。 Lambda 表达式主要用作匿名函数以作为参数传递给其他函数(请参阅匿名函数),或存储为符号函数定义以生成命名函数(请参阅命名函数)。

13.2.3 参数列表的特点

我们的简单示例函数 (lambda (abc) (+ abc)) 指定了三个参数变量,因此必须用三个参数调用它:如果你试图只用两个参数或四个参数调用它,你会得到一个错误的数字-of-arguments 错误(请参阅错误)。

编写一个允许省略某些参数的函数通常很方便。 例如,函数 substring 接受三个参数——字符串、开始索引和结束索引——但如果省略第三个参数,则默认为字符串的长度。 某些函数也可以方便地接受不定数量的参数,就像函数 list 和 + 所做的那样。

要指定在调用函数时可以省略的可选参数,只需在可选参数之前包含关键字 &optional 即可。 要指定零个或多个额外参数的列表,请在最后一个参数之前包含关键字 &rest。

因此,参数列表的完整语法如下:

(required-vars…
 [&optional [optional-vars…]]
 [&rest rest-var])

方括号表示 &optional 和 &rest 子句以及它们后面的变量是可选的。

对函数的调用需要每个必需变量的一个实际参数。 可能有零个或多个可选变量的实际参数,除此之外不能有任何实际参数,除非 lambda 列表使用 &rest。 在这种情况下,可能有任意数量的额外实际参数。

如果省略了可选变量和剩余变量的实际参数,则它们始终默认为 nil。 该函数无法区分 nil 的显式参数和省略的参数。 但是,函数体可以自由地将 nil 视为其他一些有意义值的缩写。 这就是子字符串的作用; nil 作为 substring 的第三个参数意味着使用提供的字符串的长度。

Common Lisp 注意:Common Lisp 允许函数指定在省略可选参数时使用的默认值; Emacs Lisp 总是使用 nil。 Emacs Lisp 不支持提供的-p 变量来告诉您参数是否被显式传递。

例如,如下所示的参数列表:

(a b &optional c d &rest e)

将 a 和 b 绑定到前两个实际参数,这是必需的。 如果提供了一个或两个以上参数,则 c 和 d 分别绑定到它们; 前四个之后的任何参数都被收集到一个列表中,并且 e 绑定到该列表。 因此,如果只有两个参数,c、d 和 e 为零; 如果两个或三个参数,d 和 e 为零; 如果四个参数或更少,e 为零。 请注意,恰好为 e 提供了具有显式 nil 参数的五个参数将导致该 nil 参数作为具有一个元素 (nil) 的列表传递,与 e 的任何其他单个值一样。

没有办法在可选参数后面加上必需的参数——这是没有意义的。 要了解为什么必须如此,假设示例中的 c 是可选的,而 d 是必需的。 假设给出了三个实际参数; 第三个参数用于哪个变量? 它将用于 c 还是 d? 人们可以为这两种可能性争论不休。 同样,在 &rest 参数之后再添加任何参数(必需的或可选的)也没有任何意义。

以下是参数列表和正确调用的一些示例:

(funcall (lambda (n) (1+ n))        ; One required:
	   1)                         ; requires exactly one argument.
     ⇒ 2
(funcall (lambda (n &optional n1)   ; One required and one optional:
	     (if n1 (+ n n1) (1+ n))) ; 1 or 2 arguments.
	   1 2)
     ⇒ 3
(funcall (lambda (n &rest ns)       ; One required and one rest:
	     (+ n (apply '+ ns)))     ; 1 or more arguments.
	   1 2 3 4 5)
     ⇒ 15

13.2.4 函数的文档字符串

lambda 表达式可以选择在 lambda 列表之后有一个文档字符串。 该字符串不影响函数的执行; 它是一种注释,是一种系统化的注释,它实际上出现在 Lisp 世界中,并且可以被 Emacs 帮助工具使用。 请参阅文档,了解如何访问文档字符串。

为程序中的所有函数提供文档字符串是一个好主意,即使是那些仅从程序中调用的函数。 文档字符串类似于注释,只是它们更易于访问。

文档字符串的第一行应该独立存在,因为 apropos 只显示第一行。 它应该由一两个完整的句子组成,总结了函数的目的。

文档字符串的开头通常在源文件中缩进,但由于这些空格位于起始双引号之前,它们不是字符串的一部分。 有些人习惯于缩进字符串的任何其他行,以便文本在程序源中对齐。 这是一个错误。 以下行的缩进在字符串内部; 当帮助命令显示时,源代码中看起来不错的东西看起来很难看。

您可能想知道文档字符串如何是可选的,因为它后面有函数的必需组件(主体)。 由于字符串的评估返回该字符串,没有任何副作用,如果它不是正文中的最后一个形式,则它没有任何效果。 因此,在实践中,正文的第一种形式和文档字符串之间没有混淆; 如果唯一的主体形式是一个字符串,那么它既可以用作返回值,也可以用作文档。

文档字符串的最后一行可以指定不同于实际函数参数的调用约定。 像这样写文本:

\(fn arglist)

在行首的空行之后,文档字符串中没有换行符。 (’' 用于避免混淆 Emacs 运动命令。)以这种方式指定的调用约定出现在帮助消息中,代替从函数的实际参数派生的调用约定。

此功能对宏定义特别有用,因为宏定义中编写的参数通常与用户对宏调用部分的看法不符。

如果您想弃用调用约定并支持您按上述规范宣传的调用约定,请不要使用此功能。 相反,使用advertised-calling-convention 声明(参见声明表单)或set-advertised-calling-convention(参见声明过时函数),因为这两个将导致字节编译器在编译Lisp程序时发出警告消息已弃用的调用约定。

13.3 命名函数

符号可以作为函数的名称。 当符号的函数单元(参见符号组件)包含函数对象(例如,lambda 表达式)时,就会发生这种情况。 然后符号本身成为一个有效的、可调用的函数,相当于其函数单元格中的函数对象。

函数单元格的内容也称为符号的函数定义。 使用符号的函数定义代替符号的过程称为符号函数间接; 请参阅符号函数间接。 如果你没有给符号一个函数定义,那么它的函数单元就被称为是无效的,并且它不能被用作一个函数。

在实践中,几乎所有函数都有名称,并通过它们的名称来引用。 您可以通过定义 lambda 表达式并将其放入函数单元格来创建命名 Lisp 函数(请参阅访问函数单元格内容)。 但是,更常见的是使用 defun 特殊形式,将在下一节中介绍。 请参阅定义函数。

我们给函数命名是因为在 Lisp 表达式中通过它们的名称来引用它们很方便。 此外,一个命名的 Lisp 函数可以很容易地引用它自己——它可以是递归的。 此外,原语只能通过它们的名称在文本中引用,因为原语函数对象(请参阅原语函数类型)没有读取语法。

函数不需要有唯一的名称。 一个给定的函数对象通常只出现在一个符号的函数单元格中,但这只是一种约定。 使用 fset 很容易将其存储在多个符号中; 那么每个符号都是同一函数的有效名称。

请注意,用作函数名的符号也可以用作变量; 符号的这两种用法是独立的,并不冲突。 (在某些 Lisp 方言中,情况并非如此,例如 Scheme。)

按照惯例,如果一个函数的符号由两个用“–”分隔的名称组成,则该函数是供内部使用的,第一部分命名定义该函数的文件。 例如,名为 vc-git–rev-parse 的函数是 vc-git.el 中定义的内部函数。 用 C 编写的内部使用函数的名称以“-internal”结尾,例如 bury-buffer-internal。 2018 年之前贡献的 Emacs 代码可能遵循其他内部使用的命名约定,这些约定正在逐步淘汰。

13.4 定义函数

我们通常在首次创建函数时为其命名。 这称为定义函数,我们通常使用 defun 宏来完成。 本节还介绍了定义函数的其他方法。

Macro: defun name args [doc] [declare] [interactive] body… ¶

defun 是定义新的 Lisp 函数的常用方法。 它将符号名称定义为具有参数列表 args 的函数(请参阅参数列表的特征)和 body 给出的主体形式。 name 和 args 都不应该被引用。

doc,如果存在,应该是一个字符串,指定函数的文档字符串(请参阅函数文档字符串)。 如果存在,则声明应该是指定函数元数据的声明表单(请参阅声明表单)。 交互,如果存在,应该是一个交互形式,指定如何交互调用函数(参见交互调用)。

defun 的返回值是未定义的。

这里有些例子:

     (defun foo () 5)
     (foo)
	   ⇒ 5


     (defun bar (a &optional b &rest c)
	  (list a b c))
     (bar 1 2 3 4 5)
	   ⇒ (1 2 (3 4 5))

     (bar 1)
	   ⇒ (1 nil nil)

     (bar)
     error→ Wrong number of arguments.


     (defun capitalize-backwards ()
	"Upcase the last letter of the word at point."
	(interactive)
	(backward-word 1)
	(forward-word 1)
	(backward-char 1)
	(capitalize-word 1))

注意不要无意中重新定义现有功能。 defun 甚至毫不犹豫地重新定义了汽车等原始功能。 Emacs 不会阻止你这样做,因为重新定义一个函数有时是故意的,没有办法区分故意的重新定义和无意的重新定义。

Function: defalias name definition &optional doc ¶

该函数将符号名称定义为一个函数,带有定义定义(可以是任何有效的 Lisp 函数)。 它的返回值是未定义的。

如果 doc 不为 nil,则成为 name 的函数文档。 否则,将使用定义提供的任何文档。

在内部,defalias 通常使用 fset 来设置定义。 但是,如果 name 具有 defalias-fset-function 属性,则关联的值将用作函数来代替 fset 调用。

使用 defalias 的正确位置是定义特定函数名称的地方——尤其是该名称显式出现在正在加载的源文件中的地方。 这是因为 defalias 记录了哪个文件定义了函数,就像 defun 一样(参见卸载)。

相比之下,在为其他目的操作函数定义的程序中,最好使用 fset,它不会保留此类记录。 请参阅访问函数单元格内容。

您不能使用 defun 或 defalias 创建新的原始函数,但您可以使用它们来更改任何符号的函数定义,即使是诸如 car 或 x-popup-menu 之类的正常定义为原始符号的符号。 然而,这是有风险的:例如,在不完全破坏 Lisp 的情况下重新定义汽车几乎是不可能的。 重新定义诸如 x-popup-menu 之类的晦涩功能的危险性较小,但它仍然可能无法按您预期的那样工作。 如果从 C 代码调用原语,它们会直接调用原语的 C 定义,因此更改符号的定义不会对它们产生影响。

另见 defsubst,它定义了一个类似于 defun 的函数,并告诉 Lisp 编译器对其执行内联扩展。 请参阅内联函数。

要取消定义函数名称,请使用 fmakunbound。 请参阅访问函数单元格内容。

13.5 调用函数

定义功能只是成功的一半。 函数在您调用它们之前不会做任何事情,即告诉它们运行。 调用函数也称为调用。

调用函数的最常见方法是评估列表。 例如,评估列表 (concat “a” “b”) 调用带有参数 “a” 和 “b” 的函数 concat。 有关评估的说明,请参阅评估。

当您在程序中将列表编写为表达式时,您可以在程序的文本中指定要调用的函数以及要为其提供多少参数。 通常这正是你想要的。 有时您需要在运行时计算要调用的函数。 为此,请使用函数 funcall。 当您还需要在运行时确定要传递多少个参数时,请使用 apply。

Function: funcall function &rest arguments ¶

funcall 使用参数调用函数,并返回函数返回的任何内容。

由于 funcall 是一个函数,因此它的所有参数,包括函数,都会在调用 funcall 之前进行评估。 这意味着您可以使用任何表达式来获取要调用的函数。 这也意味着 funcall 不会看到您为参数编写的表达式,而只会看到它们的值。 在调用函数的行为中,这些值不会被第二次评估; funcall 的操作就像调用函数的正常过程一样,一旦它的参数已经被评估。

参数函数必须是 Lisp 函数或原始函数。 不允许使用特殊形式和宏,因为它们只有在给定未计算的参数表达式时才有意义。 funcall 无法提供这些,因为正如我们在上面看到的,它从一开始就永远不知道它们。

如果您需要使用 funcall 来调用命令并使其表现得像交互式调用一样,请使用 funcall-interactively(请参阅交互式调用)。



  (setq f 'list)
	   ⇒ list

  (funcall f 'x 'y 'z)
	   ⇒ (x y z)

  (funcall f 'x 'y '(z))
	   ⇒ (x y (z))

  (funcall 'and t nil)
  error→ Invalid function: #<subr and>

将这些示例与 apply 的示例进行比较。

Function: apply function &rest arguments ¶

apply 使用参数调用函数,就像 funcall 但有一个区别:最后一个参数是对象列表,它们作为单独的参数而不是单个列表传递给函数。 我们说 apply 扩展这个列表,以便每个单独的元素成为一个参数。

带有单个参数的 apply 是特殊的:参数的第一个元素必须是一个非空列表,它作为一个函数调用,其余元素作为单独的参数。 传递两个或更多参数会更快。

apply 返回调用函数的结果。 与 funcall 一样,函数必须是 Lisp 函数或原始函数; 特殊形式和宏在 apply 中没有意义。

  (setq f 'list)
	   ⇒ list

  (apply f 'x 'y 'z)
  error→ Wrong type argument: listp, z

  (apply '+ 1 2 '(3 4))
	   ⇒ 10

  (apply '+ '(1 2 3 4))
	   ⇒ 10


  (apply 'append '((a b c) nil (x y z) nil))
	   ⇒ (a b c x y z)


  (apply '(+ 3 4))
	   ⇒ 7

有关使用 apply 的有趣示例,请参阅 mapcar 的定义。

有时将函数的某些参数固定为某些值是很有用的,而将其余参数留给函数实际调用时使用。 固定一些函数参数的行为称为函数的部分应用。 结果是一个新函数,它接受其余参数并调用原始函数并将所有参数组合在一起。

以下是如何在 Emacs Lisp 中执行部分应用程序:

Function: apply-partially func &rest args ¶

此函数返回一个新函数,当调用该函数时,将调用 func 并使用由 args 和调用时指定的附加参数组成的参数列表。 如果 func 接受 n 个参数,那么使用 m <= n 个参数调用 apply-partially 将产生一个具有 n - m 个参数的新函数12。

下面是我们如何定义内置函数 1+,如果它不存在,使用 apply-partially 和 +,另一个内置函数 13:

     (defalias '1+ (apply-partially '+ 1)
	"Increment argument by one.")

     (1+ 10)
	   ⇒ 11

Lisp 函数通常接受函数作为参数或在数据结构中找到它们(尤其是在钩子变量和属性列表中)并使用 funcall 或 apply 调用它们。 接受函数参数的函数通常称为函数。

有时,当您调用函数时,提供一个无操作函数作为参数很有用。 这里有两种不同的无操作函数:

Function: identity argument ¶

此函数返回参数并且没有副作用。

Function: ignore &rest arguments ¶

此函数忽略任何参数并返回 nil。

Function: always &rest arguments ¶

此函数忽略任何参数并返回 t。

有些函数是用户可见的命令,可以交互调用(通常通过按键序列)。 通过使用 call-interactively 函数,可以完全调用这样的命令,就好像它被交互式调用一样。 请参阅交互式呼叫。 脚注 (11)

这与 currying 相关但不同,currying 将接受多个参数的函数转换为可以作为函数链调用的函数,每个函数都有一个参数。 (12)

如果 func 可以接受的参数数量是无限的,那么新函数也将接受无限数量的参数,因此在这种情况下 apply-partially 不会减少新函数可以接受的参数数量。 (13)

请注意,与内置函数不同,此版本接受任意数量的参数。

13.6 映射函数

映射函数将给定函数(不是特殊形式或宏)应用于列表或其他集合的每个元素。 Emacs Lisp 有几个这样的函数; 本节介绍 mapcar、mapc、mapconcat 和 mapcan,它们在列表上进行映射。 有关映射 obarray 中符号的函数 mapatoms,请参见 mapatoms 的定义。 有关映射哈希表中键/值关联的函数 maphash,请参见 maphash 的定义。

这些映射函数不允许使用字符表,因为字符表是一个稀疏数组,其标称索引范围非常大。 要以适当处理其稀疏性质的方式映射 char-table,请使用函数 map-char-table(请参阅 Char-Tables)。

Function: mapcar function sequence ¶

mapcar 依次对序列的每个元素应用函数,并返回结果列表。

参数序列可以是除字符表之外的任何类型的序列; 即列表、向量、布尔向量或字符串。 结果始终是一个列表。 结果的长度与序列的长度相同。 例如:



     (mapcar #'car '((a b) (c d) (e f)))
	   ⇒ (a c e)
     (mapcar #'1+ [1 2 3])
	   ⇒ (2 3 4)
     (mapcar #'string "abc")
	   ⇒ ("a" "b" "c")


     ;; Call each function in my-hooks.
     (mapcar 'funcall my-hooks)


     (defun mapcar* (function &rest args)
	"Apply FUNCTION to successive cars of all ARGS.
     Return the list of results."
	;; If no list is exhausted,
	(if (not (memq nil args))
	    ;; apply function to CARs.
	    (cons (apply function (mapcar #'car args))
		  (apply #'mapcar* function
			 ;; Recurse for rest of elements.
			 (mapcar #'cdr args)))))


     (mapcar* #'cons '(a b c) '(1 2 3 4))
	   ⇒ ((a . 1) (b . 2) (c . 3))
Function: mapcan function sequence ¶

此函数将函数应用于序列的每个元素,如 mapcar,但不是将结果收集到列表中,而是通过更改结果(使用 nconc;请参阅重新排列列表的函数)。 与 mapcar 一样,序列可以是除字符表之外的任何类型。

  ;; Contrast this:
  (mapcar #'list '(a b c d))
	   ⇒ ((a) (b) (c) (d))
  ;; with this:
  (mapcan #'list '(a b c d))
	   ⇒ (a b c d)
Function: mapc function sequence ¶

mapc 与 mapcar 类似,只是该函数仅用于副作用——它返回的值被忽略,而不是收集到列表中。 mapc 总是返回序列。

Function: mapconcat function sequence separator ¶

mapconcat 将函数应用于序列的每个元素; 结果,必须是字符序列(字符串、向量或列表),被连接成单个字符串返回值。 在每对结果序列之间,mapconcat 从分隔符插入字符,分隔符也必须是字符串、向量或字符列表。 请参阅序列、数组和向量。

参数函数必须是一个可以接受一个参数并返回一系列字符的函数:字符串、向量或列表。 参数序列可以是除字符表之外的任何类型的序列; 即列表、向量、布尔向量或字符串。

   (mapconcat #'symbol-name
		 '(The cat in the hat)
		 " ")
	   ⇒ "The cat in the hat"


   (mapconcat (lambda (x) (format "%c" (1+ x)))
		 "HAL-8000"
		 "")
	   ⇒ "IBM.9111"

13.7 匿名函数

尽管函数通常同时使用 defun 和给定名称定义,但有时使用显式 lambda 表达式(匿名函数)会很方便。 匿名函数在函数名所在的地方都是有效的。 它们通常被分配为变量值,或作为函数的参数; 例如,您可以将一个作为函数参数传递给 mapcar,该函数将该函数应用于列表的每个元素(请参阅映射函数)。 请参阅 describe-symbols 示例,了解一个实际的示例。

在定义用作匿名函数的 lambda 表达式时,原则上可以使用任何方法来构造列表。 但通常你应该使用 lambda 宏,或者特殊形式的函数,或者 #’ 读取语法:

Macro: lambda args [doc] [interactive] body… ¶

此宏返回一个匿名函数,其中包含参数列表 args、文档字符串 doc(如果有)、交互式规范交互(如果有)和 body 给出的正文形式。

在动态绑定下,此宏有效地使 lambda 表单自引用:评估 CAR 为 lambda 的表单会产生表单本身:

  (lambda (x) (* x x))
	   ⇒ (lambda (x) (* x x))

请注意,在词法绑定下进行评估时,结果是一个闭包对象(请参阅闭包)。

lambda 形式还有另一个效果:它通过将函数用作子例程(见下文)告诉 Emacs 求值器和字节编译器它的参数是一个函数。

Special Form: function function-object ¶

这种特殊形式返回函数对象而不对其进行评估。 在这方面,它类似于引用(参见引用)。 但与引用不同的是,它还可以作为 Emacs 评估器和字节编译器的注释,说明函数对象旨在用作函数。 假设 function-object 是一个有效的 lambda 表达式,这有两个效果:

当代码被字节编译时,函数对象被编译成字节码函数对象(参见字节编译)。 当启用词法绑定时,函数对象被转换为闭包。 请参阅闭包。

当函数对象是一个符号并且代码是字节编译时,如果该函数未定义或在运行时可能不知道,字节编译器将发出警告。

读取语法 #’ 是使用函数的简写。 以下形式都是等价的:

(lambda (x) (* x x))
(function (lambda (x) (* x x)))
#'(lambda (x) (* x x))

在下面的示例中,我们定义了一个 change-property 函数,该函数将一个函数作为其第三个参数,然后是一个双属性函数,该函数通过向其传递一个匿名函数来使用 change-property:

(defun change-property (symbol prop function)
  (let ((value (get symbol prop)))
    (put symbol prop (funcall function value))))


(defun double-property (symbol prop)
  (change-property symbol prop (lambda (x) (* 2 x))))

请注意,我们不引用 lambda 形式。

如果编译上面的代码,匿名函数也会被编译。 如果您通过将匿名函数引用为列表来构造匿名函数,则不会发生这种情况:

(defun double-property (symbol prop)
  (change-property symbol prop '(lambda (x) (* 2 x))))

在这种情况下,匿名函数将作为 lambda 表达式保存在编译的代码中。 字节编译器不能假定这个列表是一个函数,即使它看起来像一个,因为它不知道 change-property 打算将它用作一个函数。

13.8 泛型函数

使用 defun 定义的函数对其参数的类型和预期值有一组硬编码假设。 例如,如果使用任何其他类型的值(例如向量或字符串)调用其参数值(数字或数字列表)的函数,该函数将失败或发出错误信号。 发生这种情况是因为函数的实现没有准备好处理设计期间假定的类型以外的类型。

相比之下,面向对象的程序使用多态函数:一组具有相同名称的专用函数,每个函数都是为一组特定的参数类型编写的。 实际调用哪个函数是在运行时根据实际参数的类型决定的。

Emacs 提供对多态性的支持。 与其他 Lisp 环境一样,尤其是 Common Lisp 及其 Common Lisp 对象系统 (CLOS),这种支持基于通用函数。 Emacs 泛型函数紧跟 CLOS,包括使用相似的名称,所以如果您有 CLOS 的经验,本节的其余部分听起来会非常熟悉。

泛型函数通过定义其名称和参数列表来指定抽象操作,但(通常)没有实现。 几个特定类的参数的实际实现由方法提供,这些方法应该单独定义。 实现泛型函数的每个方法都与泛型函数具有相同的名称,但是方法的定义通过专门化泛型函数定义的参数来指示它可以处理哪些类型的参数。 这些论点专家可能或多或少是具体的。 例如,字符串类型比更一般的类型(如序列)更具体。

请注意,与基于消息的 OO 语言(例如 C++ 和 Simula)不同,实现泛型函数的方法不属于一个类,它们属于它们实现的泛型函数。

调用泛型函数时,它通过将调用者传递的实际参数与每个方法的参数专用器进行比较来选择适用的方法。 如果调用的实际参数与方法的专用程序兼容,则该方法适用。 如果有不止一种方法适用,则使用某些规则将它们组合在一起,如下所述,然后组合处理调用。

Macro: cl-defgeneric name arguments [documentation] [options-and-methods…] &rest body ¶

此宏定义具有指定名称和参数的通用函数。 如果 body 存在,它提供默认实现。 如果存在文档(应该总是存在),它会以 (:documentation docstring) 的形式指定通用函数的文档字符串。 可选的选项和方法可以是以下形式之一:

(:method [qualifiers…] args &rest body)

声明表格,如声明表格中所述。

(:argument-precedence-order &rest args)

这种形式会影响组合适用方法的排序顺序。 通常,在组合过程中比较两个方法时,从左到右检查方法参数,并且参数专门化器更具体的第一个方法将排在另一个之前。 这种形式定义的顺序会覆盖它,并且根据它们在这种形式中的顺序检查参数,而不是从左到右。

(:method [qualifiers…] args &rest body)

这种形式定义了一个类似 cl-defmethod 的方法。

Macro: cl-defmethod name [extra] [qualifier] arguments [&context (expr spec)…] &rest [docstring] body ¶

该宏定义了名为 name 的通用函数的特定实现。 实现代码由 body 给出。 如果存在,则 docstring 是该方法的文档字符串。 参数列表在实现泛型函数的所有方法中必须相同,并且必须与该函数的参数列表匹配,提供形式为 (arg spec) 的参数专用器,其中 arg 是在 cl 中指定的参数名称-defgeneric 调用,而 spec 是以下特殊形式之一:

type

此专用程序要求参数为给定类型,是下面描述的类型层次结构中的类型之一。

(eql object)

此专门工具要求参数是给定对象的 eql。

(head object)

参数必须是一个 cons 单元格,其 car 是 eql 到 object。

struct-type

参数必须是使用 cl-defstruct 定义的名为 struct-type 的类的实例(请参阅 GNU Emacs Lisp 的 Common Lisp Extensions 中的结构)或其子类之一。

方法定义可以使用新的参数列表关键字 &context,它引入了额外的专门工具,在方法运行时测试环境。 此关键字应出现在必需参数列表之后,但在任何 &rest 或 &optional 关键字之前。 &context 专用器看起来很像常规参数专用器(expr spec),除了 expr 是要在当前上下文中评估的表达式,而 spec 是要比较的值。 例如,&context (overwrite-mode (eql t)) 将使该方法仅在打开 overwrite-mode 时适用。 &context 关键字后面可以跟任意数量的上下文特化器。 因为上下文特化器不是泛型函数的参数签名的一部分,所以它们可以在不需要它们的方法中被省略。

类型专用器 (arg type) 可以指定以下列表中的系统类型之一。 当指定父类型时,类型是其更具体的子类型中的任何一个的参数,以及孙子、孙子孙等也将是兼容的。

integer

父类型:数字。

number
null

父类型:符号

symbol
string

父类型:数组。

array

父类型:序列。

cons

父类型:列表。

list

父类型:序列。

marker
overlay
float

父类型:数字。

window-configuration
process
window
subr
compiled-function
buffer
char-table

父类型:数组。

bool-vector

父类型:数组。

vector

父类型:数组。

frame
hash-table
font-spec
font-entity
font-object

可选的额外元素,表示为 ‘:extra string’,允许您为相同的专用符和限定符添加更多方法,以字符串区分。

可选限定符允许组合几种适用的方法。 如果不存在,则定义的方法是主要方法,负责为专用参数提供泛型函数的主要实现。 您还可以使用以下值之一作为限定符来定义辅助方法:

:before

此辅助方法将在主要方法之前运行。 更准确地说,所有 :before 方法都将在主要方法之前以最具体的优先顺序运行。

:after

此辅助方法将在主要方法之后运行。 更准确地说,所有这些方法都将在主要方法之后以最具体的最后顺序运行。

:around

此辅助方法将代替主要方法运行。 最具体的此类方法将在任何其他方法之前运行。 此类方法通常使用 cl-call-next-method(如下所述)来调用其他辅助或主要方法。

使用 cl-defmethod 定义的函数不能通过向它们添加交互形式来实现交互,即命令(参见定义命令)。 如果您需要多态命令,我们建议定义一个普通命令,该命令调用通过 cl-defgeneric 和 cl-defmethod 定义的多态函数。

每次调用泛型函数时,它都会通过组合为函数定义的适用方法来构建将处理此调用的有效方法。 寻找适用方法并产生有效方法的过程称为调度。 适用的方法是那些其所有特工都与调用的实际参数兼容的方法。 由于所有参数都必须与专用器兼容,因此它们都决定了方法是否适用。 显式特化多个参数的方法称为多分派方法。

适用的方法按它们组合的顺序排序。 最左边的参数专门化器是最具体的方法将按顺序排在第一位。 (指定 :argument-precedence-order 作为 cl-defmethod 的一部分会覆盖它,如上所述。)如果方法主体调用 cl-call-next-method,则将运行下一个最具体的方法。 如果有适用的 :around 方法,它们中最具体的将首先运行; 它应该调用 cl-call-next-method 来运行任何不太具体的 :around 方法。 接下来,:before 方法按照它们的特殊性顺序运行,然后是主要方法,最后是 :after 方法,按照它们特殊性的相反顺序运行。

Function: cl-call-next-method &rest args ¶

当从主方法或 :around 辅助方法的词法体中调用时,为同一个泛型函数调用下一个适用的方法。 通常,它被调用时不带参数,这意味着使用与调用方法相同的参数来调用下一个适用的方法。 否则,将使用指定的参数。

Function: cl-next-method-p ¶

当从主方法或 :around 辅助方法的词法体中调用此函数时,如果有下一个方法要调用,则返回非 nil。

13.9 访问函数单元格内容

符号的函数定义是存储在符号的函数单元中的对象。 此处描述的功能访问、测试和设置符号的功能单元。

另见函数间接函数。 请参见间接函数的定义。

Function: symbol-function symbol ¶

这将返回符号函数单元格中的对象。 它不检查返回的对象是否是合法函数。

如果函数单元格为 void,则返回值为 nil。 要区分为 void 的函数单元格和设置为 nil 的函数单元格,请使用 fboundp(见下文)。



  (defun bar (n) (+ n 2))
  (symbol-function 'bar)
	   ⇒ (lambda (n) (+ n 2))

  (fset 'baz 'bar)
	   ⇒ bar

  (symbol-function 'baz)
	   ⇒ bar

如果您从未给符号任何函数定义,我们说该符号的函数单元格是无效的。 换句话说,函数单元格中没有任何 Lisp 对象。 如果您尝试将符号作为函数调用,Emacs 会发出 void-function 错误信号。

请注意,void 与 nil 或符号 void 不同。 符号 nil 和 void 是 Lisp 对象,并且可以像任何其他对象一样存储到函数单元中(如果您依次使用 defun 定义它们,它们可以是有效函数)。 空函数单元格不包含任何对象。

您可以使用 fboundp 测试符号函数定义的无效性。 给符号定义函数后,可以使用 fmakunbound 再次使其无效。

Function: fboundp symbol ¶

如果符号在其函数单元格中有对象,则此函数返回 t,否则返回 nil。 它不检查对象是否是合法函数。

Function: fmakunbound symbol ¶

此函数使符号的函数单元格无效,因此随后尝试访问此单元格将导致无效函数错误。 它返回符号。 (另见 makunbound,在当变量为空时。)

  (defun foo (x) x)
  (foo 1)
	   ⇒1

  (fmakunbound 'foo)
	   ⇒ foo

  (foo 1)
  error→ Symbol's function definition is void: foo
Function: fset symbol definition ¶

该函数将定义存储在符号的函数单元中。 结果是定义。 通常定义应该是一个函数或一个函数的名称,但这不被检查。 参数符号是一个普通的评估参数。

此函数的主要用途是作为定义或更改函数的构造的子例程,例如 defun 或advice-add(请参阅Advising Emacs Lisp Functions)。 您还可以使用它为符号提供不是函数的函数定义,例如键盘宏(请参阅键盘宏):

  ;; Define a named keyboard macro.
  (fset 'kill-two-lines "\^u2\^k")
	   ⇒ "\^u2\^k"

如果您希望使用 fset 为函数创建备用名称,请考虑改用 defalias。 请参见defalias 的定义。

13.10 闭包

正如变量绑定的范围规则中所解释的,Emacs 可以选择启用变量的词法绑定。 启用词法绑定后,您创建的任何命名函数(例如,使用 defun)以及您使用 lambda 宏或函数特殊形式或 #’ 语法(请参阅匿名函数)创建的任何匿名函数都会自动转换为闭包。

闭包是一个函数,它还带有定义函数时存在的词法环境的记录。 当它被调用时,其定义中的任何词法变量引用都使用保留的词法环境。 在所有其他方面,闭包的行为很像普通函数。 特别是,它们可以像普通函数一样被调用。

有关使用闭包的示例,请参见词法绑定。

目前,Emacs Lisp 闭包对象由一个列表表示,其中符号闭包作为第一个元素,一个表示词法环境的列表作为第二个元素,参数列表和主体形式作为其余元素:

;; lexical binding is enabled.
(lambda (x) (* x x))
     ⇒ (closure (t) (x) (* x x))

然而,闭包的内部结构暴露给 Lisp 世界的其余部分这一事实被认为是内部实现细节。 因此,我们建议不要直接检查或更改闭包对象的结构。

13.11 建议 Emacs Lisp 函数

当您需要修改在另一个库中定义的函数时,或者当您需要修改诸如 foo 函数、进程过滤器之类的钩子,或者基本上任何包含函数值的变量或对象字段时,您可以使用适当的 setter 函数,例如 fset 或 defun 用于命名函数, setq 用于钩子变量,或 set-process-filter 用于流程过滤器,但这些通常过于生硬,完全丢弃了以前的值。

建议功能允许您通过建议函数来添加到函数的现有定义。 这是比重新定义整个函数更简洁的方法。

Emacs 的建议系统为此提供了两组原语:核心集,用于保存在变量和对象字段中的函数值(相应的原语是 add-function 和 remove-function),另一组在其之上分层用于命名函数(主要原语是建议添加和建议删除)。

作为一个简单的例子,下面是如何添加建议,以在每次调用函数时修改函数的返回值:

(defun my-double (x)
  (* x 2))
(defun my-increase (x)
  (+ x 1))
(advice-add 'my-double :filter-return #'my-increase)

添加此建议后,如果您使用 ‘3’ 调用 my-double,则返回值将是 ‘7’。 要删除此建议,请说

(advice-remove 'my-double #'my-increase)

一个更高级的示例是跟踪对进程 proc 的进程过滤器的调用:

(defun my-tracing-function (proc string)
  (message "Proc %S received %S" proc string))

(add-function :before (process-filter proc) #'my-tracing-function)

这将导致进程的输出在传递给原始进程过滤器之前传递给 my-tracing-function。 my-tracing-function 接收与原始函数相同的参数。 完成后,您可以通过以下方式恢复未跟踪的行为:

(remove-function (process-filter proc) #'my-tracing-function)

同样,如果要跟踪名为 display-buffer 的函数的执行,可以使用:

(defun his-tracing-function (orig-fun &rest args)
  (message "display-buffer called with args %S" args)
  (let ((res (apply orig-fun args)))
    (message "display-buffer returned %S" res)
    res))

(advice-add 'display-buffer :around #'his-tracing-function)

在这里,他的跟踪函数被调用而不是原始函数,并接收原始函数(除了该函数的参数)作为参数,因此它可以在需要时调用它。 当您厌倦了看到此输出时,您可以通过以下方式恢复未跟踪的行为:

(advice-remove 'display-buffer #'his-tracing-function)

上面示例中使用的参数 :before 和 :around 指定了这两个函数的组合方式,因为有许多不同的方法可以做到这一点。 添加的功能也称为一条建议。

13.11.1 操纵建议的原语

Macro: add-function where place function &optional props ¶

这个宏是将通知函数添加到存储在适当位置的函数的便捷方法(请参阅通用变量)。

where 确定函数如何与现有函数组合,例如,函数应该在原始函数之前还是之后调用。 有关组合这两个函数的可用方法列表,请参阅编写建议的方法。

当修改一个变量(其名称通常以 -function 结尾)时,您可以选择函数是全局使用还是仅在当前缓冲区中使用:如果 place 只是一个符号,则将 function 添加到 place 的全局值中。 而如果 place 是 (local symbol) 形式,其中 symbol 是返回变量名的表达式,则函数只会添加到当前缓冲区中。 最后,如果要修改词法变量,则必须使用 (var variable)。

每个使用 add-function 添加的函数都可以附带一个属性 props 的关联列表。 目前只有两个属性具有特殊含义:

name

这为建议提供了一个名称,remove-function 可以使用该名称来识别要删除的函数。 通常在函数是匿名函数时使用。

depth

如果存在多条建议,这指定了如何对建议进行排序。 默认情况下,深度为 0。深度 100 表示这条建议应该尽可能保持深度,而深度 -100 表示它应该保持在最外层。 当两条建议指定相同的深度时,最近添加的一条将位于最外层。

对于 :before 建议,最外层意味着该建议将首先运行,在任何其他建议之前,而最内层意味着它将在原始函数之前运行,在其自身和原始函数之间没有其他建议运行。 类似地,for :after 建议最内意味着它将在原始函数之后运行,中间没有其他建议运行,而最外意味着它将在所有其他建议之后立即运行。 最里面的 :override 建议只会覆盖原始函数,其他建议将应用于它,而最外面的 :override 建议不仅会覆盖原始函数,还会覆盖应用于它的所有其他建议。

如果函数不是交互的,那么组合函数将继承原始函数的交互规范(如果有的话)。 否则,组合功能将是交互式的,并将使用功能的交互规范。 一个例外:如果函数的交互规范是一个函数(即 lambda 表达式或 fbound 符号而不是表达式或字符串),那么组合函数的交互规范将是使用交互规范调用该函数原始函数作为唯一参数。 要将收到的规范解释为参数,请使用advice-eval-interactive-spec。

注意:函数的交互规范将适用于组合函数,因此应遵守组合函数的调用约定,而不是函数的调用约定。 在许多情况下,因为它们是相同的,所以没有区别,但对于 :around、:filter-args 和 :filter-return 来说确实很重要,其中函数接收的参数与存储在适当位置的原始函数不同。

Macro: remove-function place function ¶

此宏从存储在适当位置的函数中删除函数。 这仅在使用 add-function 将函数添加到位置时才有效。

函数与使用等于添加到位置的函数进行比较,以尝试使其也适用于 lambda 表达式。 它还与添加到 place 的函数的 name 属性进行比较,这比使用 equal 比较 lambda 表达式更可靠。

Function: advice-function-member-p advice function-def ¶

如果通知已经在函数定义中,则返回非零。 就像上面的 remove-function 一样,advice 不是实际的函数,它也可以是一条通知的名称。

Function: advice-function-mapc f function-def ¶

为添加到 function-def 的每条建议调用函数 f。 f 使用两个参数调用:advice 函数及其属性。

Function: advice-eval-interactive-spec spec ¶

评估交互式规范,就像对具有此类规范的函数的交互式调用一样,然后返回已构建的相应参数列表。 例如,(advice-eval-interactive-spec “r\nP”) 将返回一个包含三个元素的列表,其中包含区域的边界和当前的前缀参数。

例如,如果您想让 Cx m(撰写邮件)命令提示符为“发件人:”标头,您可以这样说:

     (defun my-compose-mail-advice (orig &rest args)
	"Read From: address interactively."
	(interactive
	 (lambda (spec)
	   (let* ((user-mail-address
		   (completing-read "From: "
				    '("one.address@example.net"
				      "alternative.address@example.net")))
		  (from (message-make-from user-full-name
					   user-mail-address))
		  (spec (advice-eval-interactive-spec spec)))
	     ;; Put the From header into the OTHER-HEADERS argument.
	     (push (cons 'From from) (nth 2 spec))
	     spec)))
	(apply orig args))

     (advice-add 'compose-mail :around #'my-compose-mail-advice)

13.11.2 建议命名函数

建议的常见用途是命名函数和宏。 您可以只使用 add-function ,如下所示:

(add-function :around (symbol-function 'fun) #'his-tracing-function)

但是你应该使用advice-add 和advice-remove 来代替。 这组单独的函数用于操作应用于命名函数的建议片段,与 add-function 相比,它们提供了以下额外功能:它们知道如何处理宏和自动加载的函数,它们让 describe-function 保留原始文档字符串和文档添加的建议,它们允许您在定义函数之前添加和删除建议。

建议添加可用于更改对现有函数的现有调用的行为,而无需重新定义整个函数。 但是,它可能是错误的来源,因为该函数的现有调用者可能会假定旧的行为,并且当行为被建议更改时无法正常工作。 如果进行调试的人没有注意到或记得函数已被建议修改,建议也会导致调试混乱。

由于这些原因,建议应保留在您无法以任何其他方式修改函数行为的情况下。 如果可以通过钩子做同样的事情,那是最好的(见钩子)。 如果您只是想更改特定键的功能,最好编写一个新命令,并将旧命令的键绑定重新映射到新命令(请参阅重新映射命令)。

如果您正在编写发布代码以供他人使用,请尽量避免在其中包含建议。 如果您要建议的函数没有钩子来完成这项工作,请与 Emacs 开发人员讨论添加合适的钩子。 特别是,Emacs 自己的源文件不应该对 Emacs 中的函数提供建议。 (目前这个约定有一些例外,但我们的目标是纠正它们。)通常在 foo 中创建一个新的钩子,并让 bar 使用该钩子,而不是让 bar 在 foo 中放置建议。

不能建议特殊形式(请参阅特殊形式),但可以建议使用宏,其方式与函数大致相同。 当然,这不会影响已经宏扩展的代码,因此您需要确保在宏扩展之前安装了通知。

可以建议一个原语(请参阅什么是函数?),但通常不应该这样做,原因有两个。 首先,通知机制使用了一些原语,通知它们可能会导致无限递归。 其次,许多原语是直接从 C 中调用的,这样的调用会忽略通知; 因此,最终会陷入一种令人困惑的情况,其中一些调用(来自 Lisp 代码)遵循建议,而其他调用(来自 C 代码)则不遵循。

Macro: define-advice symbol (where lambda-list &optional name depth) &rest body ¶

该宏定义了一条建议并将其添加到名为 symbol 的函数中。 如果 name 为 nil 或名为 symbol@name 的函数,则建议是匿名函数。 有关其他参数的解释,请参阅advice-add。

Function: advice-add symbol where function &optional props ¶

将通知函数添加到命名函数符号。 where 和 props 与 add-function 的含义相同(请参阅 Primitives 以操作建议)。

Function: advice-remove symbol function ¶

从命名函数符号中删除通知函数。 function 也可以是一条建议的名称。

Function: advice-member-p function symbol ¶

如果通知函数已经在命名函数符号中,则返回非零。 function 也可以是一条建议的名称。

Function: advice-mapc function symbol ¶

为添加到命名函数符号的每条建议调用函数。 使用两个参数调用函数:建议函数及其属性。

13.11.3 编写建议的方法

以下是 add-function 和advice-add 的where 参数的不同可能值,指定了advice 函数和原始函数的组合方式。

:before

在旧函数之前调用函数。 两个函数接收相同的参数,组合的返回值是旧函数的返回值。 更具体地说,这两个函数的组合行为如下:

(lambda (&rest r) (apply function r) (apply oldfun r))

(add-function :before funvar function) 与普通钩子的 (add-hook ‘hookvar function) 相当。

:after

在旧函数之后调用函数。 两个函数接收相同的参数,组合的返回值是旧函数的返回值。 更具体地说,这两个函数的组合行为如下:

(lambda (&rest r) (prog1 (apply oldfun r) (apply function r)))

(add-function :after funvar function) 对于单功能挂钩与 (add-hook ‘hookvar function ‘append) 对于普通挂钩相当。

:override

这完全用新功能替换了旧功能。 如果您稍后调用 remove-function,旧功能当然可以恢复。

:around

调用函数而不是旧函数,但提供旧函数作为函数的额外参数。 这是最灵活的组合。 例如,它允许您使用不同的参数调用旧函数,或者多次调用,或者在 let-binding 中调用,或者您有时可以将工作委托给旧函数,有时完全覆盖它。 更具体地说,这两个函数的组合行为如下:

(lambda (&rest r) (apply function oldfun r))
:before-while

在旧函数之前调用函数,如果函数返回 nil,则不要调用旧函数。 两个函数接收相同的参数,组合的返回值是旧函数的返回值。 更具体地说,这两个函数的组合行为如下:

(lambda (&rest r) (and (apply function r) (apply oldfun r)))

(add-function :before-while funvar function) 当 hookvar 通过 run-hook-with-args-until-failure 运行时,单函数钩子与 (add-hook ‘hookvar function) 相当。

:before-until

在旧函数之前调用函数,并且仅在函数返回 nil 时才调用旧函数。 更具体地说,这两个函数的组合行为如下:

(lambda (&rest r) (or (apply function r) (apply oldfun r)))

(add-function :before-until funvar function) 当 hookvar 通过 run-hook-with-args-until-success 运行时,单函数钩子与 (add-hook ‘hookvar function) 相当。

:after-while

在旧函数之后调用函数,并且仅当旧函数返回非零时。 两个函数接收相同的参数,组合的返回值是函数的返回值。 更具体地说,这两个函数的组合行为如下:

(lambda (&rest r) (and (apply oldfun r) (apply function r)))

(add-function :after-while funvar function) 当 hookvar 通过 run-hook-with-args-until-failure 运行时,单函数钩子与 (add-hook ‘hookvar function ‘append) 相当。

:after-until

在旧函数之后调用函数,并且仅当旧函数返回 nil 时。 更具体地说,这两个函数的组合行为如下:

(lambda (&rest r) (or  (apply oldfun r) (apply function r)))

(add-function :after-until funvar function) 当 hookvar 通过 run-hook-with-args-until-success 运行时,单函数钩子与 (add-hook ‘hookvar function ‘append) 相当。

:filter-args

首先调用函数并将结果(应该是一个列表)作为新参数传递给旧函数。 更具体地说,这两个函数的组合行为如下:

(lambda (&rest r) (apply oldfun (funcall function r)))
:filter-return

首先调用旧函数并将结果传递给函数。 更具体地说,这两个函数的组合行为如下:

(lambda (&rest r) (funcall function (apply oldfun r)))

13.11.4 使用旧的 defadvice 适配代码

很多代码使用旧的 defadvice 机制,新的advice-add 在很大程度上使这种机制过时了,它的实现和语义要简单得多。

一条古老的建议,例如:

 (defadvice previous-line (before next-line-at-end
				   (&optional arg try-vscroll))
   "Insert an empty line when moving up from the top line."
   (if (and next-line-add-newlines (= arg 1)
	     (save-excursion (beginning-of-line) (bobp)))
	(progn
	  (beginning-of-line)
	  (newline))))

可以在新的建议机制中翻译成一个简单的函数:

(defun previous-line--next-line-at-end (&optional arg try-vscroll)
  "Insert an empty line when moving up from the top line."
  (if (and next-line-add-newlines (= arg 1)
	   (save-excursion (beginning-of-line) (bobp)))
      (progn
	(beginning-of-line)
	(newline))))

显然,这实际上并没有修改上一行。 为此,需要旧的建议:

(ad-activate 'previous-line)

而新的建议机制需要:

(advice-add 'previous-line :before #'previous-line--next-line-at-end)

请注意 ad-activate 具有全局效果:它激活了为该指定功能启用的所有建议。 如果您只想激活或停用特定部分,则需要使用 ad-enable-advice 和 ad-disable-advice 启用或禁用它。 新机制消除了这种区别。

周围的建议,例如:

(defadvice foo (around foo-around)
  "Ignore case in `foo'."
  (let ((case-fold-search t))
    ad-do-it))
(ad-activate 'foo)

可以翻译成:

(defun foo--foo-around (orig-fun &rest args)
  "Ignore case in `foo'."
  (let ((case-fold-search t))
    (apply orig-fun args)))
(advice-add 'foo :around #'foo--foo-around)

关于通知的类,请注意新的 :before 并不完全等同于旧的 before,因为在旧的通知中您可以修改函数的参数(例如,使用 ad-set-arg),这会影响看到的参数值通过原始函数,而在新的 :before 中,通过通知中的 setq 修改参数对原始函数看到的参数没有影响。 在移植依赖于这种行为的通知之前,您需要将其转换为新的 :around 或 :filter-args 通知。

类似地,旧的 after 通知可以通过更改 ad-return-value 来修改返回值,而新的 :after 通知不能,因此在移植这种旧的 after 通知时,您需要将其转换为新的 :around 或 :filter-return 通知.

13.12 声明过时的函数

您可以将命名函数标记为过时,这意味着它可能会在将来的某个时候被删除。 这会导致 Emacs 在对包含该函数的代码进行字节编译时以及在显示该函数的文档时警告该函数已过时。 在所有其他方面,过时的函数的行为与任何其他函数一样。

将函数标记为过时的最简单方法是将 (declare (obsolete …)) 形式放入函数的 defun 定义中。 请参阅声明表格。 或者,您可以使用 make-obsolete 函数,如下所述。

宏(参见宏)也可以用 make-obsolete 标记为过时的; 这与函数具有相同的效果。 函数或宏的别名也可以标记为过时; 这使得别名本身已经过时,而不是它解析为的函数或宏。

Function: make-obsolete obsolete-name current-name when ¶

此函数将过时的名称标记为过时。 obsolete-name 应该是命名函数或宏的符号,或者是函数或宏的别名。

如果 current-name 是一个符号,则警告消息说使用 current-name 而不是 obsolete-name。 current-name 不需要是 obsolete-name 的别名; 它可以是具有相似功能的不同功能。 current-name 也可以是一个字符串,用作警告信息。 消息应以小写字母开头,并以句点结尾。 它也可以为 nil,在这种情况下,警告消息不提供其他详细信息。

参数 when 应该是一个字符串,指示函数第一次被废弃的时间——例如,日期或版本号。

Macro: define-obsolete-function-alias obsolete-name current-name when &optional doc ¶

此便利宏将函数 obsolete-name 标记为已过时,并将其定义为函数 current-name 的别名。 它等价于以下内容:

(defalias obsolete-name current-name doc)
(make-obsolete obsolete-name current-name when)

此外,您可以将函数的特定调用约定标记为过时:

Function: set-advertised-calling-convention function signature when ¶

此函数将参数列表签名指定为调用函数的正确方式。 这会导致 Emacs 字节编译器在遇到以任何其他方式调用函数的 Emacs Lisp 程序时发出警告(但是,它仍然允许对代码进行字节编译)。 when 应该是一个字符串,指示变量第一次被废弃的时间(通常是版本号字符串)。

例如,在旧版本的 Emacs 中,sit-for 函数接受三个参数,像这样

(sit-for seconds milliseconds nodisp)

但是,以这种方式调用 sit-for 被认为是过时的(请参阅等待经过的时间或输入)。 不推荐使用旧的调用约定,如下所示:

     (set-advertised-calling-convention
	'sit-for '(seconds &optional nodisp) "22.1")

13.13 内联函数

内联函数是一个像普通函数一样工作的函数,除了一件事:当您对函数的调用进行字节编译时(请参阅字节编译),函数的定义会扩展到调用者。

定义内联函数的简单方法是编写 defsubst 而不是 defun。 定义的其余部分看起来一样,但使用 defsubst 表示使其内联以进行字节编译。

Macro: defsubst name args [doc] [declare] [interactive] body… ¶

这个宏定义了一个内联函数。 它的语法与 defun 完全相同(参见定义函数)。

使函数内联通常会使其函数调用运行得更快。 但它也有缺点。 一方面,它降低了灵活性; 如果您更改函数的定义,则已内联的调用仍会使用旧定义,直到您重新编译它们。

另一个缺点是内联大函数会增加文件和内存中编译代码的大小。 由于内联函数的速度优势对于小函数来说是最大的,所以您通常不应该将大函数内联。

此外,内联函数在调试、跟踪和建议方面表现不佳(请参阅建议 Emacs Lisp 函数)。 由于易于调试和重新定义函数的灵活性是 Emacs 的重要特性,因此即使函数很小,也不应该将函数内联,除非它的速度非常关键,并且您已经对代码进行了计时以验证使用 defun 确实具有性能问题。

定义内联函数后,可以稍后在同一个文件中执行其内联扩展,就像宏一样。

可以使用 defmacro 定义一个宏,以扩展为内联函数将执行的相同代码(请参阅宏)。 但是宏将仅限于直接在表达式中使用——宏不能用 apply、mapcar 等调用。 此外,将普通函数转换为宏需要一些工作。 将其转换为内联函数很容易; 只需将 defun 替换为 defsubst 即可。 由于内联函数的每个参数只计算一次,因此您不必担心函数体使用参数的次数,就像对宏所做的那样。

或者,您可以通过提供将其内联为编译器宏的代码来定义函数。 以下宏使这成为可能。

Macro: define-inline name args [doc] [declare] body… ¶

通过提供执行其内联的代码(作为编译器宏)来定义函数名称。 该函数将接受参数列表 args 并具有指定的主体。

如果存在,doc 应该是函数的文档字符串(请参阅函数文档字符串); 如果存在,则声明应该是一个声明表单(请参阅声明表单),指定函数的元数据。

通过 define-inline 定义的函数相对于 defsubst 或 defmacro 定义的宏有几个优点:

  • 它们可以传递给 mapcar(参见映射函数)。
  • 他们更有效率。
  • 它们可以用作存储值的地方表格(请参阅广义变量)。
  • 它们的行为方式比 cl-defsubst 更可预测(请参阅 Common Lisp Extensions for GNU Emacs Lisp 中的参数列表)。

与 defmacro 一样,使用 define-inline 内联的函数从调用站点继承范围规则,无论是动态的还是词法的。 请参阅变量绑定的范围规则。

以下宏应该用在由define-inline 定义的函数体中。

Macro: inline-quote expression ¶

内联定义的引用表达式。 这类似于反引号(参见反引号),但引用代码并且只接受 , 不接受 ,@。

Macro: inline-letevals (bindings…) body… ¶

这提供了一种方便的方法来确保内联函数的参数只被评估一次,以及创建局部变量。

它类似于 let(请参阅局部变量):它设置绑定指定的局部变量,然后使用有效的绑定评估 body。

绑定的每个元素都应该是一个符号或形式的列表(var expr); 结果是评估 expr 并将 var 绑定到结果。 但是,当绑定的元素只是一个符号 var 时,评估 var 的结果会重新绑定到 var(这与 let 的工作方式完全不同)。

绑定的尾部可以是 nil 或应该包含参数列表的符号,在这种情况下,每个参数都被评估,并且符号被绑定到结果列表。

Macro: inline-const-p expression ¶

如果表达式的值已知,则返回非零。

Macro: inline-const-val expression ¶

返回表达式的值。

Macro: inline-error format &rest args ¶

发出错误信号,根据格式格式化参数。

下面是一个使用define-inline的例子:

(define-inline myaccessor (obj)
  (inline-letevals (obj)
    (inline-quote (if (foo-p ,obj) (aref (cdr ,obj) 3) (aref ,obj 2)))))

这相当于

(defsubst myaccessor (obj)
  (if (foo-p obj) (aref (cdr obj) 3) (aref obj 2)))

13.14 declare形式

declare 是一个特殊的宏,可用于向函数或宏添加元属性:例如,将其标记为过时,或在 Emacs Lisp 模式下为其形式提供特殊的 TAB 缩进约定。

Macro: declare specs… ¶

这个宏忽略它的参数并计算为 nil; 它没有运行时影响。 但是,当 defun 或 defsubst 函数定义(请参阅定义函数)或 defmacro 宏定义(请参阅定义宏)的 declare 参数中出现声明形式时,它会将 specs 指定的属性附加到函数或宏。 这项工作由 defun、defsubst 和 defmacro 专门执行。

specs 中的每个元素都应具有 (property args…) 形式,不应被引用。 它们具有以下效果:

(advertised-calling-convention signature when)

这就像对 set-advertised-calling-convention 的调用(请参阅声明过时的函数); 签名指定调用函数或宏的正确参数列表,以及何时应该是一个字符串,指示旧参数列表何时首次过时。

(debug edebug-form-spec)

这仅对宏有效。 使用 Edebug 单步执行宏时,请使用 edebug-form-spec。 请参阅检测宏调用。

(doc-string n)

这在定义一个函数或宏时使用,该函数或宏本身将用于定义函数、宏或变量等实体。 它表示第 n 个参数(如果有)应被视为文档字符串。

(indent indent-spec)

根据 indent-spec 缩进对此函数或宏的调用。 这通常用于宏,尽管它也适用于函数。 请参阅缩进宏。

(interactive-only value)

将函数的仅交互属性设置为 value。 请参阅仅交互属性。

(obsolete current-name when)

将函数或宏标记为过时,类似于调用 make-obsolete(请参阅声明函数过时)。 current-name 应该是一个符号(在这种情况下,警告消息说要使用它)、一个字符串(指定警告消息)或 nil(在这种情况下,警告消息没有提供额外的细节)。 when 应该是一个字符串,指示函数或宏何时首次过时。

(compiler-macro expander)

这只能用于函数,并告诉编译器使用扩展器作为优化函数。 当遇到对函数的调用时,形式为 (function args…),宏扩展器将使用该形式以及 args… 调用扩展器,并且扩展器可以返回一个新表达式以代替函数调用,或者它可以只返回未更改的形式,以指示函数调用应该不理会。 扩展器可以是一个符号,也可以是一个形式(lambda (arg) body),在这种情况下,arg 将保存原始函数调用表达式,并且可以使用函数的形式参数访问函数的(未计算的)参数。

(gv-expander expander)

将扩展器声明为将宏(或函数)调用处理为广义变量的函数,类似于 gv-define-expander。 扩展器可以是一个符号,也可以是 (lambda (arg) body) 形式,在这种情况下,该函数还可以访问宏(或函数)的参数。

(gv-setter setter)

将 setter 声明为将宏(或函数)调用处理为广义变量的函数。 setter 可以是一个符号,在这种情况下它将被传递给 gv-define-simple-setter,或者它可以是 (lambda (arg) body) 的形式,在这种情况下,该函数将另外可以访问宏(或函数) 的参数,它将被传递给 gv-define-setter。

(completion completion-predicate)

将完成谓词声明为函数,以确定在 Mx 中请求完成时是否将符号包含在函数列表中。 使用两个参数调用完成谓词:第一个参数是符号,第二个参数是当前缓冲区。

(modes modes)

指定此命令仅适用于模式。

13.15 告诉编译器定义了一个函数

字节编译文件通常会产生有关编译器不知道的函数的警告(请参阅编译器错误)。 有时这表明一个真正的问题,但通常有问题的函数是在其他文件中定义的,如果该代码运行,这些文件将被加载。 例如,字节编译 simple.el 用于警告:

simple.el:8727:1:Warning: the function ‘shell-mode’ is not known to be
    defined.

实际上,shell-mode 只在调用 shell-mode 之前执行(需要 ‘shell)的函数中使用,因此 shell-mode 将在运行时正确定义。 当您知道这样的警告并不表示真正的问题时,最好抑制警告。 这使得可能意味着实际问题的新警告更加明显。 你可以用声明函数来做到这一点。

您需要做的就是在第一次使用相关函数之前添加一个 declare-function 语句:

(declare-function shell-mode "shell" ())

这表示 shell 模式是在 shell.el 中定义的(“.el”可以省略)。 编译器理所当然地认为该文件确实定义了函数,并且不检查。

可选的第三个参数指定 shell-mode 的参数列表。 在这种情况下,它不接受任何参数(nil 与不指定值不同)。 在其他情况下,这可能类似于(文件和可选覆盖)。 您不必指定参数列表,但如果您这样做,字节编译器可以检查调用是否与声明匹配。

Macro: declare-function function file &optional arglist fileonly ¶

告诉字节编译器假设函数是在文件文件中定义的。 可选的第三个参数 arglist 要么是 t,即未指定参数列表,要么是与 defun 样式相同的形式参数列表。 省略的 arglist 默认为 t,而不是 nil; 这是省略参数的非典型行为,这意味着要提供第四个但不提供第三个参数,必须为第三个参数占位符指定 t 而不是通常的 nil。 可选的第四个参数 fileonly non-nil 表示只检查该文件是否存在,而不是它实际上定义了函数。

要验证这些函数是否确实在 declare-function 所说的位置声明,请使用 check-declare-file 检查一个源文件中的所有 declare-function 调用,或使用 check-declare-directory 检查某个文件中和下的所有文件目录。

这些命令使用locate-library查找应该包含函数定义的文件; 如果没有找到文件,它们会扩展相对于包含声明函数调用的文件目录的定义文件名。

您还可以通过指定以“.c”或“.m”结尾的文件名来将函数称为原语。 这仅在您调用仅在某些系统上定义的原语时才有用。 大多数原语总是被定义的,所以它们永远不会给你警告。

有时文件会选择性地使用外部包中的函数。 如果在 declare-function 语句中的文件名前加上 ‘ext:’ ,则将检查是否找到,否则跳过而不会出错。

有一些’check-declare’ 不理解的函数定义(例如,defstruct 和其他一些宏)。 在这种情况下,您可以将一个非零的 fileonly 参数传递给 declare-function,这意味着只检查文件是否存在,而不是它实际定义了函数。 请注意,要在不必指定参数列表的情况下执行此操作,您应该将 arglist 参数设置为 t(因为 nil 表示空参数列表,而不是未指定的)。

13.16 判断一个函数是否可以安全调用

一些主要模式,例如 SES,调用存储在用户文件中的函数。 (有关 SES 的更多信息,请参阅 (ses)Simple Emacs 电子表格。)用户文件的谱系有时很差——您可以从刚认识的人那里获得电子表格,也可以通过从未见过的人的电子邮件获得电子表格. 因此,在您确定它是安全的之前,调用其源代码存储在用户文件中的函数是有风险的。

Function: unsafep form &optional unsafep-vars ¶

如果 form 是一个安全的 Lisp 表达式,则返回 nil,或者返回一个描述它可能不安全的原因的列表。 参数 unsafep-vars 是一个已知在此时具有临时绑定的符号列表; 它主要用于内部递归调用。 当前缓冲区是一个隐式参数,它提供了缓冲区本地绑定的列表。

由于快速而简单,unsafep 进行了非常简单的分析,并拒绝了许多实际上是安全的 Lisp 表达式。 没有已知的情况下 unsafep 为不安全的表达式返回 nil。 但是,一个安全的 Lisp 表达式可以返回一个带有 display 属性的字符串,其中包含一个关联的 Lisp 表达式,该表达式将在字符串插入缓冲区后执行。 这种相关的表达可以是病毒。 为了安全起见,您必须先从用户代码计算的所有字符串中删除属性,然后再将它们插入缓冲区。

13.17 其他与函数相关的话题

下面是几个函数的表格,这些函数执行与函数调用和函数定义相关的事情。 它们记录在其他地方,但我们在此处提供交叉引用。

apply

请参阅调用函数。

autoload

请参阅自动加载。

call-interactively

请参阅交互式呼叫。

called-interactively-p

请参阅区分交互式呼叫。

commandp

请参阅交互式呼叫。

documentation

请参阅访问文档字符串。

eval

见评估。

funcall

请参阅调用函数。

function

请参阅匿名函数。

ignore

请参阅调用函数。

indirect-function

请参阅符号函数间接。

interactive

请参阅使用交互式。

interactive-p

请参阅区分交互式呼叫。

mapatoms

请参阅创建和嵌入符号。

mapcar

请参阅映射函数。

map-char-table

请参阅字符表。

mapconcat

请参阅映射函数。

undefined

请参阅键查找函数。

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

搜索帮助