elisp快速指南

列表处理

对未曾接触过的人来说,Lisp 是一种奇怪的编程语言。在 Lisp 代码中,到处都是括号。有些人甚至戏称其名称代表“Lots of Isolated Silly Parentheses”(大量孤立的愚蠢括号)。但这种说法是没有依据的。 Lisp 实际上代表的是“LISt Processing”(列表处理),这门编程语言通过将列表(以及列表的列表)置于括号之间来处理它们。括号标志着列表的边界。有时列表前会有一个单引号‘’’,在 Lisp 中称为单引号。列表是 Lisp 的基础。

Lisp 中的列表

在 Lisp 中,一个列表看起来像这样:’(rose violet daisy buttercup)。这个列表前有一个单引号。它也可以写成如下形式,看起来更像你可能熟悉的那种列表:

1
2
3
4
'(rose
violet
daisy
buttercup)

这个列表的元素是四种不同的花的名称,彼此之间用空白分隔,并被括号包围,就像田野里用石墙围起来的花朵一样。

数字,列表中的列表

列表中也可以包含数字,如在这个列表中:(+ 2 2)。这个列表包含一个加号 ‘+’,后面跟着两个数字 ‘2’,它们之间用空格分隔。

在 Lisp 中,数据和程序的表示方式是相同的;也就是说,它们都是由单词、数字或其他列表组成的列表,彼此之间用空格分隔,并被括号包围。(由于程序看起来像数据,一个程序很容易作为另一个程序的数据使用;这是 Lisp 的一个非常强大的功能。)

这里是另一个列表,这次其中包含一个列表:

1
'(this list has (a list inside of it))

这个列表的组件是单词 ‘this’、‘list’、‘has’ 和列表 ‘(a list inside of it)’。内部列表由单词 ‘a’、‘list’、‘inside’、‘of’、‘it’ 组成。

Lisp中的原子

在Lisp中,我们所称的“单词”被称为“原子”。这个术语源自“原子”一词的历史含义,意思是“不可分割”。就Lisp而言,我们在列表中使用的单词不能再分解为更小的部分,否则它们在程序中的意义就不再相同;数字和像‘+’这样的单字符符号也是如此。另一方面,与古代原子不同,列表可以被分解为多个部分。(参见car、cdr和cons基本函数。)

在一个列表中,原子之间用空格分隔。它们可以紧挨着括号。

从技术上讲,Lisp中的列表由括号组成,括号内包含由空格分隔的原子、其他列表或同时包含原子和其他列表。一个列表可以只有一个原子,也可以什么都没有。一个空列表看起来像这样:`()`,被称为“空列表”。与其他任何东西不同,空列表同时被视为原子和列表。

原子和列表的打印表示被称为符号表达式,或更简洁地称为s-表达式。单独的“表达式”一词可以指打印表示,也可以指计算机内部存储的原子或列表。通常,人们会随意使用“表达式”这一术语。(另外,在许多文本中,“形式”一词也被用作“表达式”的同义词。)

顺便提一下,当原子被认为是不可分割时,它们才被命名为“原子”;但后来发现物理原子并非不可分割。原子的一部分可以分裂出来,或者它可以裂变成两部分,且这两部分大小大致相等。物理原子的命名过于仓促,在其真实本质被发现之前就被命名了。在Lisp中,某些类型的原子,如数组,可以分为多个部分;但实现这一操作的机制与分裂列表的机制不同。就列表操作而言,列表中的原子是不可分割的。

与英语类似,Lisp 原子的组成字母的含义与这些字母组合成单词后的含义是不同的。例如,南美洲树懒的名称“ai”与两个单词“a”和“i”的含义完全不同。

在自然界中有很多种原子,但在 Lisp 中只有几种:例如,数字,如 37、511 或 1729;以及符号,如‘+’、‘foo’或‘forward-line’。我们在上面例子中列出的单词都是符号。在日常的 Lisp 对话中,“原子” 这个词不常用,因为程序员通常会更具体地说明他们正在处理的原子类型。 Lisp 编程主要涉及列表中的符号(有时也包括数字)。顺便说一下,前面三个单词构成的括号中的注释是一个符合 Lisp 规范的列表,因为它由原子(在这种情况下是符号)组成,这些符号用空格分隔并用括号括起来,没有任何非 Lisp 的标点符号。

双引号中的文本——即使是句子或段落——也是一个原子。以下是一个例子:

1
'(this list includes "text between quotation marks.")

在 Lisp 中,所有被引号包围的文本,包括标点符号和空格,都被视为一个单一的原子。这种原子称为字符串(“字符串”),通常用于计算机打印给人类阅读的消息。字符串是一种与数字或符号不同的原子,其使用方式也不同。

列表中的空白

列表中的空白量并不重要。从 Lisp 语言的角度来看:

1
2
'(this list
 looks like this)

与这个列表是完全相同的:

1
'(this list looks like this)

这两个例子展示的在 Lisp 中是同一个列表,即由符号‘this’、‘list’、‘looks’、‘like’和‘this’按顺序组成的列表。

额外的空白和换行是为了让列表更易于人类阅读。当 Lisp 读取表达式时,它会去除所有多余的空白(但在原子之间至少需要有一个空格,以便区分它们)。

虽然看起来有点奇怪,但我们看到的这些例子几乎涵盖了 Lisp 列表的所有形式!在 Lisp 中的其他列表或多或少都类似于这些例子,只是列表可能会更长、更复杂。简而言之,一个列表在括号之间,一个字符串在引号之间,一个符号看起来像一个单词,一个数字看起来像一个数字。(在某些情况下,方括号、点和其他一些特殊字符可能会被使用;然而,我们在大多数情况下都不需要它们。)

GNU Emacs 帮助你编写列表

当你在 GNU Emacs 中使用 Lisp 交互模式或 Emacs Lisp 模式输入 Lisp 表达式时,你可以使用多个命令来格式化 Lisp 表达式,使其更易于阅读。例如,按下 TAB 键会自动将光标所在的行缩进到正确的位置。通常,将代码区域正确缩进的命令绑定在 M-C-\ 键上。缩进的设计使你能够清楚地看到列表的哪些元素属于哪个列表——子列表的元素比外部列表的元素缩进更多。

此外,当你输入一个右括号时,Emacs 会短暂地将光标跳回到匹配的左括号处,以便你确认它对应的是哪个左括号。这非常有用,因为在 Lisp 中你输入的每个列表都必须确保右括号与左括号匹配。(有关 Emacs 模式的更多信息,请参见《GNU Emacs 手册》中的“主要模式”部分。)

运行程序

在 Lisp 中,任何列表都是一个准备运行的程序。如果你运行它(在 Lisp 中的术语是“求值”),计算机将执行以下三种情况之一:什么都不做,只是返回列表本身;向你发送错误消息;或者,将列表中的第一个符号视为命令并执行某些操作。

我在前面部分的一些示例列表前面加的单引号 `’` 被称为“引用”;当它位于列表前时,它告诉 Lisp 对列表不做任何处理,只是按原样接受它。但是,如果列表前没有引用符号,列表的第一个元素是特殊的:它是计算机必须执行的命令。(在 Lisp 中,这些命令被称为“函数”。)上面显示的列表 `(+ 2 2)` 前面没有引用符号,因此 Lisp 理解 `+` 是一个指令,要求对列表的其余部分进行处理:将后续的数字相加。

如果你在 GNU Emacs 的 Info 中阅读此内容,通过这样对列表进行求值:将光标放在以下列表的右括号之后,然后键入 `C-x C-e`:

1
(+ 2 2)

你会看到数字 4 出现在回显区域(你刚刚做的就是对列表求值。回显区域是屏幕底部显示或回显文本的那一行)。现在,尝试对一个带有引用符号的列表做同样的操作:将光标放在以下列表的右括号之后,然后键入 `C-x C-e`:

1
'(this is a quoted list)

你会看到 `(this is a quoted list)` 出现在回显区域。

在这两种情况下,你所做的都是向 GNU Emacs 内的一个名为 Lisp 解释器的程序发出命令,要求解释器对表达式进行求值。 Lisp 解释器的名称源自于一个人为表达式赋予意义并解释它的任务。

你也可以对不在列表中的原子进行求值——即那些没有被括号包围的原子;同样,Lisp 解释器会将人类可读的表达式翻译成计算机语言。但在讨论这个之前(见变量),我们将先讨论当你出错时 Lisp 解释器会做什么。

生成错误消息

为了让你在不小心做错时不会过于担心,我们现在将向 Lisp 解释器发出一个命令,以生成错误消息。这是一个无害的操作;实际上,我们经常会故意生成错误消息。一旦你理解了术语,错误消息实际上是很有帮助的。与其称它们为“错误”消息,不如称它们为“帮助”消息。它们就像在陌生国度中的路标;虽然解读它们可能很难,但一旦理解了,它们就能指明方向。

错误消息由 GNU Emacs 内置的调试器生成。我们将进入调试器。你可以通过输入 `q` 退出调试器。

我们要做的是对一个未引用且第一个元素不是有效命令的列表进行求值。下面是一个几乎与我们刚刚使用的列表完全相同的列表,但没有前面的单引号。将光标放在列表之后,然后输入 `C-x C-e`:

1
(this is an unquoted list)

一个 Backtrace 窗口将会打开,你应该在其中看到以下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
---------- Buffer: *Backtrace* ----------
Debugger entered--Lisp error: (void-function this)
  (this is an unquoted list)
  eval((this is an unquoted list) nil)
  elisp--eval-last-sexp(nil)
  eval-last-sexp(nil)
  funcall-interactively(eval-last-sexp nil)
  call-interactively(eval-last-sexp nil nil)
  command-execute(eval-last-sexp)
---------- Buffer: *Backtrace* ----------

你的光标将位于这个窗口中(你可能需要等待几秒钟才会看到它)。要退出调试器并关闭调试器窗口,输入:q

基于我们已经知道的内容,我们几乎可以理解这个错误消息。

你应该从下往上读取 Backtrace 缓冲区;它告诉你 Emacs 做了什么。当你输入 C-x C-e 时,你进行了对命令 eval-last-sexp 的交互调用。 eval 是 “evaluate”(求值)的缩写,而 sexp 是 “symbolic expression”(符号表达式)的缩写。该命令的意思是 “求值最后一个符号表达式”,也就是光标前面的那个表达式。

上面的每一行告诉你 Lisp 解释器接下来求值的内容。最近的操作在最上面。这个缓冲区被称为 Backtrace 缓冲区,因为它让你能够追踪 Emacs 的操作顺序。

在 Backtrace 缓冲区的顶部,你会看到这一行:

1
Debugger entered--Lisp error: (void-function this)

Lisp 解释器试图求值列表的第一个原子,即单词 ’this’。正是这个操作产生了错误消息 ‘void-function this’。

消息中包含了 ‘void-function’ 和 ’this’ 这两个词。

‘function’ 这个词之前提到过一次。它是一个非常重要的词。对于我们的目的,我们可以这样定义它:函数是一组给计算机的指令,告诉计算机执行某些操作。

现在我们可以开始理解这个错误消息:‘void-function this’。函数(即单词 ’this’)没有定义任何一组指令让计算机执行。

这个略显奇怪的词语 ‘void-function’ 是为了说明 Emacs Lisp 的实现方式,即当一个符号没有附加函数定义时,应该包含指令的位置是空的。

另一方面,由于我们能够通过求值 (+ 2 2) 成功地得到 2 加 2 的结果,我们可以推断出符号 + 必定有一组指令让计算机遵循,而这些指令必须是将 `+` 后面的数字相加。

可以避免在这种情况下 Emacs 进入调试器。我们在这里不解释如何做到这一点,但我们会提到结果是什么样的,因为如果你在使用的某些 Emacs 代码中遇到错误,你可能会遇到类似的情况。在这种情况下,你只会看到一行错误消息;它会出现在回显区域,看起来像这样:

1
Symbol's function definition is void: this

当你输入一个按键时,哪怕只是移动光标,消息就会消失。

我们已经知道 “Symbol” 这个词的含义。它指的是列表的第一个原子,即单词 “this”。“function” 这个词指的是告诉计算机该做什么的指令。(从技术上讲,符号告诉计算机在哪里找到指令,但这是一个我们目前可以忽略的复杂问题。)

这个错误消息可以理解为:‘Symbol’s function definition is void: this’。符号(即单词 “this”)缺少让计算机执行的指令。

符号名称与函数定义

基于我们迄今为止讨论的内容,我们可以明确 Lisp 的另一个特征——一个重要的特征:像 + 这样的符号本身并不是计算机要执行的那组指令。相反,符号可能只是暂时用来定位定义或指令集的一种方式。我们所看到的是用来找到这些指令的名称。人的名字也是同样的道理。我可以被称为 “Bob”;然而,我并不是字母 “B”、“o”、“b”,而是(或曾经是)与某个特定生命形式一致关联的意识。名字并不是我本身,但它可以用来指代我。

在 Lisp 中,一组指令可以附加到多个名称上。例如,用于加法运算的计算机指令可以同时链接到符号 plus 和符号 + (在某些 Lisp 方言中就是如此)。在人类社会中,我可以被称为 “Robert” 或 “Bob” 以及其他名字。

另一方面,一个符号在同一时间只能有一个函数定义与之关联。否则,计算机会因为不知道使用哪个定义而感到困惑。如果在人类社会中也存在这种情况,那么世界上只有一个人可以叫 “Bob”。不过,符号名称所指向的函数定义可以轻松更改。(参见《设置函数定义》)。

由于 Emacs Lisp 规模庞大,通常习惯于为符号命名,以便识别该函数所属的 Emacs 部分。因此,处理 Texinfo 的所有函数名称都以 “texinfo-” 开头,而处理邮件读取的函数名称则以 “rmail-” 开头。

Lisp 解释器

基于我们所见的内容,我们现在可以开始理解 Lisp 解释器在我们命令它求值一个列表时的工作原理。首先,它查看列表前是否有引号;如果有,引擎解释器就直接返回列表。另一方面,如果没有引号,解释器会查看列表中的第一个元素,并检查它是否有函数定义。如果有,解释器就执行函数定义中的指令。否则,解释器会打印一条错误消息。

这就是 Lisp 的工作原理,很简单。接下来我们还会探讨一些额外的复杂情况,但这些是基本原理。当然,要编写 Lisp 程序,你需要知道如何编写函数定义并将其附加到名称上,以及如何在不让自己或计算机混淆的情况下做到这一点。

复杂情况

现在,我们来探讨第一个复杂情况。除了列表外,Lisp 解释器还可以求值一个未加引号且没有括号的符号。 Lisp 解释器会尝试将该符号的值作为变量来确定。这种情况在变量部分有描述。(参见《变量》)。

第二个复杂情况出现是因为有些函数不常见,它们的工作方式与常规方法不同。这些不常规的函数被称为特殊形式(special forms)。它们用于一些特殊的任务,比如定义函数,而且数量不多。在接下来的几章中,你将会接触到一些较为重要的特殊形式。

除了特殊形式,还有宏。宏是 Lisp 中定义的一种结构,它与函数的不同之处在于,宏将一个 Lisp 表达式翻译为另一个表达式,以替代原始表达式进行求值。(参见《Lisp 宏》)。

对于本介绍的目的,你不需要过于担心某个东西是特殊形式、宏,还是普通函数。例如,if 是一种特殊形式(参见《if 特殊形式》),但 when 是一种宏(参见《Lisp 宏》)。在早期版本的 Emacs 中,defun 是一种特殊形式,但现在它是一种宏(参见《defun 宏》)。不过,它的行为仍然相同。

最后一个复杂情况是:如果 Lisp 解释器所查看的函数不是特殊形式,并且它是列表的一部分,解释器会查看该列表中是否包含另一个列表。如果存在内部列表,Lisp 解释器首先会确定如何处理该内部列表,然后再处理外部列表。如果内部列表中还有嵌套的列表,解释器会先处理那个嵌套的列表,以此类推。它总是首先处理最内层的列表。解释器首先处理最内层的列表,以求值该列表的结果。该结果可能会被外层表达式使用。

否则,解释器会从左到右依次处理每个表达式。

字节编译

关于解释的另一个方面:Lisp 解释器能够解释两种实体:人类可读的代码(我们将专注于此),以及经过特殊处理的代码,称为字节编译代码,这种代码不可供人类阅读。字节编译代码的运行速度比人类可读代码更快。

你可以通过运行 byte-compile-file 等编译命令之一,将人类可读代码转换为字节编译代码。字节编译代码通常存储在以 .elc 为扩展名的文件中,而不是 .el 扩展名的文件中。在 emacs/lisp 目录中你会看到这两种类型的文件;需要阅读的是扩展名为 .el 的文件。

实际上,对于大多数自定义或扩展 Emacs 的操作,你不需要进行字节编译,因此我在此不会讨论这一话题。有关字节编译的完整描述,请参见《GNU Emacs Lisp 参考手册》中的字节编译部分。

求值

当 Lisp 解释器处理一个表达式时,这一活动被称为求值。我们说解释器“对表达式进行求值”。我之前已经多次使用过这个术语。根据《韦氏新大学词典》,这个词源自日常语言的用法,意为“确定价值或数量;评估”。

Lisp 解释器的行为

在对一个表达式求值之后,Lisp 解释器很可能会返回计算机通过执行函数定义中的指令所产生的值,或者可能会放弃该函数并产生一条错误消息。(解释器也可能被“抛到”另一个函数中,或者可能会试图在无限循环中不断重复它正在做的事情。这些行为较为少见,我们可以忽略它们。)最常见的是,解释器会返回一个值。

在解释器返回一个值的同时,它也可能会执行其他操作,例如移动光标或复制文件;这种类型的操作被称为副作用。对我们人类来说很重要的操作,比如打印结果,对于 Lisp 解释器来说通常是副作用。学习如何使用副作用相对容易。

总而言之,对符号表达式进行求值通常会导致 Lisp 解释器返回一个值,并可能执行一个副作用;否则会产生一个错误。

求值内部列表

如果求值作用于嵌套在另一个列表中的列表,则外部列表在求值时可能会使用第一次求值返回的值作为信息。这就解释了为什么先对内部表达式进行求值:它们返回的值会被外部表达式使用。

我们可以通过求值另一个加法示例来探究这一过程。将光标放在以下表达式之后,然后输入 C-x C-e:

1
(+ 2 (+ 3 3))

数字 8 会出现在回显区域。

发生的情况是,Lisp 解释器首先对内部表达式 (+ 3 3) 进行求值,返回的值是 6;然后,它对外部表达式求值,仿佛它是 (+ 2 6),结果返回值是 8。由于没有更多的外部表达式需要求值,解释器将该值打印在回显区域。

现在,C-x C-e 这一组合键调用的命令名称变得容易理解了:这个命令的名称是 eval-last-sexp。 sexp 是 “symbolic expression”(符号表达式)的缩写,而 eval 是 “evaluate”(求值)的缩写。该命令对最后一个符号表达式进行求值。

作为实验,你可以尝试将光标放在表达式后面的下一行的开头,或者放在表达式内部,然后对该表达式进行求值。

以下是该表达式的另一个副本:

1
(+ 2 (+ 3 3))

如果你将光标放在紧跟表达式之后的空白行的开头,并输入 C-x C-e,你仍然会在回显区域看到数字 8。现在尝试将光标放在表达式内部。如果你将光标放在倒数第二个括号之后(所以看起来它位于最后一个括号上方),你会在回显区域看到数字 6!这是因为该命令对表达式 (+ 3 3) 进行了求值。

现在将光标放在数字之后。输入 C-x C-e,你将得到这个数字本身。在 Lisp 中,如果你对一个数字进行求值,你会得到这个数字本身——这就是数字与符号的区别。如果你对以 + 等符号开头的列表进行求值,你会得到计算机执行与该名称关联的函数定义中的指令后的结果。如果对符号本身进行求值,则会发生不同的情况,我们将在下一节中看到。

变量

在 Emacs Lisp 中,一个符号可以附加一个值,就像它可以附加一个函数定义一样。两者是不同的。函数定义是一组计算机将执行的指令。而一个值则是某种可以变化的东西,比如数字或名字(这也是为什么这样的符号被称为变量)。符号的值可以是 Lisp 中的任何表达式,例如符号、数字、列表或字符串。一个有值的符号通常被称为变量。

一个符号可以同时附加函数定义和值,也可以只附加其中一个。这两者是独立的。这有点类似于“剑桥”这个名字既可以指代马萨诸塞州的城市,也可以附加一些信息,比如“著名的编程中心”。

另一种理解方式是将符号想象成一个有抽屉的柜子。函数定义放在一个抽屉里,值放在另一个抽屉里,等等。存放值的抽屉中的内容可以更改,而不会影响存放函数定义的抽屉中的内容,反之亦然。

fill-column,一个示例变量

变量 fill-column 说明了一个附加了值的符号:在每个 GNU Emacs 缓冲区中,这个符号都被设置为某个值,通常是 72 或 70,但有时也会设置为其他值。要查看这个符号的值,可以直接对其进行求值。如果你正在 GNU Emacs 中的 Info 阅读此内容,你可以将光标放在符号后面,然后按 C-x C-e:

1
fill-column

当我输入 `C-x C-e` 后,Emacs 在回显区域显示了数字 72。这是我在编写这段文字时 fill-column 被设置的值。在你的 Info 缓冲区中,这个值可能不同。请注意,作为变量返回的值和函数执行其指令后返回的值,其显示方式完全相同。从 Lisp 解释器的角度来看,返回的值就是返回的值。一旦知道了这个值,它来自什么表达式就不再重要了。

一个符号可以附加任何值,或者用行话来说,我们可以将变量绑定到一个值:可以是一个数字,比如 72;可以是一个字符串,比如 “such as this”;可以是一个列表,比如 (spruce pine oak);我们甚至可以将一个变量绑定到一个函数定义。

符号可以通过几种方式绑定到一个值。有关其中一种方式的信息,请参阅“设置变量的值”。

没有函数的符号的错误信息

当我们对 fill-column 求值以查看其作为变量的值时,没有在这个词周围加上括号。这是因为我们并不打算将其用作函数名。

如果 fill-column 是列表中的第一个或唯一元素,Lisp 解释器将尝试查找附加给它的函数定义。但 fill-column 并没有函数定义。试着对以下内容求值:

1
(fill-column)

你会创建一个名为 Backtrace 的缓冲区,其中显示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
---------- Buffer: *Backtrace* ----------
Debugger entered--Lisp error: (void-function fill-column)
  (fill-column)
  eval((fill-column) nil)
  elisp--eval-last-sexp(nil)
  eval-last-sexp(nil)
  funcall-interactively(eval-last-sexp nil)
  call-interactively(eval-last-sexp nil nil)
  command-execute(eval-last-sexp)
---------- Buffer: *Backtrace* ----------

(记住,要退出调试器并关闭调试器窗口,在 Backtrace 缓冲区中输入 q。)

没有值的符号的错误信息

如果你尝试对一个没有绑定值的符号进行求值,你将收到一条错误信息。你可以通过尝试我们的 2 加 2 来看到这一点。在以下表达式中,将光标放在 + 后面、第一个数字 2 之前,然后输入 C-x C-e:

1
(+ 2 2)

在 GNU Emacs 22 中,你会创建一个名为 Backtrace 的缓冲区,其中显示:

1
2
3
4
5
6
7
8
9
---------- Buffer: *Backtrace* ----------
Debugger entered--Lisp error: (void-variable +)
  eval(+)
  elisp--eval-last-sexp(nil)
  eval-last-sexp(nil)
  funcall-interactively(eval-last-sexp nil)
  call-interactively(eval-last-sexp nil nil)
  command-execute(eval-last-sexp)
---------- Buffer: *Backtrace* ----------

(同样,你可以通过在 Backtrace 缓冲区中输入 q 来退出调试器。)

这个回溯与我们看到的第一个错误信息不同,第一个错误信息显示为“Debugger entered–Lisp error: (void-function this)”。在这个案例中,函数没有作为变量的值;而在另一个错误信息中,函数(“this”这个词)没有定义。

在我们对 + 进行的实验中,我们让 Lisp 解释器对 + 进行求值,并查找变量的值,而不是函数定义。我们通过将光标放在符号后面,而不是像之前那样放在封闭列表的括号后面来实现这一点。结果,Lisp 解释器求值了前面的 s-表达式,在这种情况下就是 + 本身。

由于 + 没有绑定任何值,只有函数定义,因此错误信息报告该符号作为变量的值为空。

参数

为了了解信息如何传递给函数,我们再次来看一下我们熟悉的例子,即两个数相加。在 Lisp 中,这样写:

1
(+ 2 2)

如果你对这个表达式求值,数字 4 会出现在你的回显区域。Lisp 解释器的作用是将 + 后面的数字相加。

这些被 + 加起来的数字被称为函数 + 的参数。这些数字是传递给函数的信息。

“参数”一词来源于数学中的用法,并不指两个人之间的争论;相反,它指的是传递给函数的信息,在这个例子中就是传递给 + 的信息。在 Lisp 中,函数的参数是跟在函数后面的原子或列表。通过对这些原子或列表求值获得的值会传递给函数。不同的函数需要不同数量的参数,有些函数甚至不需要参数。

参数的数据类型

传递给函数的数据类型取决于函数使用的信息类型。像 + 这样的函数,其参数必须是数值,因为 + 是用来进行数值相加的。而其他函数则会使用不同类型的数据作为参数。

例如,concat 函数将两个或多个文本字符串连接在一起,生成一个新的字符串。它的参数是字符串。将两个字符串 “abc” 和 “def” 连接起来会生成单一字符串 “abcdef”。你可以通过对以下表达式求值来验证这一点:

1
(concat "abc" "def")

对这个表达式求值的结果是 “abcdef”。

像 substring 这样的函数则会同时使用字符串和数字作为参数。这个函数返回字符串的一部分,即第一个参数的子字符串。 substring 函数接受三个参数。第一个参数是字符字符串,第二个和第三个参数是数字,用于指示子字符串的起始位置(包含)和结束位置(不包含)。这些数字表示从字符串开头起字符(包括空格和标点符号)的计数。请注意,字符串中的字符编号从零开始,而不是从一开始。

例如,如果你对以下表达式求值:

1
(substring "The quick brown fox jumped." 16 19)

你会在回显区域看到 “fox”。这些参数包括字符串和两个数字。

请注意,传递给 substring 的字符串虽然由几个用空格分隔的单词组成,但它仍然是一个单一的原子。 Lisp 将两个引号之间的所有内容都视为字符串的一部分,包括空格。你可以将 substring 函数视为一种“原子粉碎机”,因为它能够从一个不可分割的原子中提取出一部分。不过,substring 只能从字符串类型的参数中提取子字符串,不能从其他类型的原子(如数字或符号)中提取。

作为变量或列表值的参数

参数可以是一个符号,当对其求值时会返回一个值。例如,当单独对符号 fill-column 求值时,它会返回一个数字。这个数字可以用于加法运算中。

将光标放在以下表达式后面,然后输入 C-x C-e:

1
(+ 2 fill-column)

结果将是一个比单独求值 fill-column 时多出 2 的数字。对我来说,这是 74,因为我的 fill-column 值是 72。

正如我们刚刚看到的,参数可以是一个符号,在求值时返回一个值。此外,参数还可以是一个列表,在求值时返回一个值。例如,在以下表达式中,函数 concat 的参数是字符串 “The " 和 " red foxes.” 以及列表 (number-to-string (+ 2 fill-column))。

1
(concat "The " (number-to-string (+ 2 fill-column)) " red foxes.")

如果你对这个表达式求值——并且如果你的 Emacs 中 fill-column 的值为 72——那么回显区域中将显示 “The 74 red foxes."。(注意,你必须在单词 “The” 后和单词 “red” 前加上空格,这样它们才会出现在最终的字符串中。 number-to-string 函数将加法函数返回的整数转换为字符串。number-to-string 也被称为 int-to-string。)

可变数量的参数

一些函数,例如 concat、+ 或 ,可以接受任意数量的参数。( 是乘法的符号。)你可以通过对以下每个表达式按常规方式求值来验证这一点。回显区域中显示的结果在文本中用 ⇒ 表示,你可以将其理解为“求值结果为”。

在第一组中,这些函数没有参数:

1
2
3
(+)        0

(*)        1

在这一组中,每个函数有一个参数:

1
2
3
(+ 3)      3

(* 3)      3

在这一组中,每个函数有三个参数:

1
2
3
(+ 3 4 5)  12

(* 3 4 5)  60

使用错误类型的对象作为参数

当一个函数被传递了错误类型的参数时,Lisp 解释器会生成一条错误消息。例如,+ 函数期望其参数的值是数字。作为实验,我们可以传递一个引用的符号 hello 而不是数字。将光标放在以下表达式后面,然后输入 C-x C-e:

1
(+ 2 'hello)

当你这样做时,会生成一条错误消息。发生的情况是,+ 试图将 2 与 hello 返回的值相加,但 hello 返回的值是符号 hello,而不是一个数字。只有数字才能相加,所以 + 无法执行加法操作。

你将创建并进入一个名为 Backtrace 的缓冲区,其中显示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
---------- Buffer: *Backtrace* ----------
Debugger entered--Lisp error:
         (wrong-type-argument number-or-marker-p hello)
  +(2 hello)
  eval((+ 2 'hello) nil)
  elisp--eval-last-sexp(t)
  eval-last-sexp(nil)
  funcall-interactively(eval-print-last-sexp nil)
  call-interactively(eval-print-last-sexp nil nil)
  command-execute(eval-print-last-sexp)
---------- Buffer: *Backtrace* ----------

像往常一样,错误信息试图提供帮助,并在你学会如何解读它之后变得容易理解。

错误消息的第一部分很直接,提示“错误的类型参数”(wrong type argument)。接下来是一个看起来神秘的术语“number-or-marker-p”。这个术语试图告诉你 + 函数期望接收到哪种类型的参数。

符号 number-or-marker-p 表示 Lisp 解释器正在尝试确定提供给它的信息(参数的值)是否为数字或标记(代表缓冲区位置的特殊对象)。它的作用是测试 + 函数是否接收到可以相加的数字。它还测试参数是否是被称为标记的东西,这是一种 Emacs Lisp 的特定功能。(在 Emacs 中,缓冲区中的位置记录为标记。当使用 C-@ 或 C-SPC 命令设置标记时,其位置作为标记保存。标记可以被视为一个数字——该位置距缓冲区起始位置的字符数。)在 Emacs Lisp 中,`+` 可以用于将标记位置的数值相加。

number-or-marker-p 中的 p 是早期 Lisp 编程实践的体现。p 代表谓词(predicate)。在早期 Lisp 研究者使用的术语中,谓词指的是一个判断某个属性是“真”还是“假”的函数。因此,p 告诉我们 number-or-marker-p 是一个函数的名称,该函数用于判断提供的参数是否为数字或标记。其他以 p 结尾的 Lisp 符号包括 zerop(一个测试其参数是否为零的函数)和 listp(一个测试其参数是否为列表的函数)。

最后,错误消息的最后一部分是符号 hello。这是传递给 + 的参数的值。如果传递给 + 的参数是正确类型的对象,这个值会是一个数字,例如 37,而不是像 hello 这样的符号。但在那种情况下,你就不会收到错误信息了。

message 函数

和 + 函数一样,message 函数也可以接受可变数量的参数。它用于向用户发送消息,非常有用,因此我们将在这里介绍它。

消息会显示在回显区域。例如,你可以通过对以下列表求值,将一条消息打印在你的回显区域中:

1
(message "This message appears in the echo area!")

双引号之间的整个字符串是一个单一的参数,并将被完整打印。(注意,在这个例子中,消息本身会在回显区域中显示在双引号内;这是因为你看到了 message 函数返回的值。在你编写的程序中,大多数使用 message 的情况下,文本会作为副作用打印在回显区域,而不会带有引号。有关此类示例,请参阅 multiply-by-seven 的详细介绍。)

然而,如果在引用的字符串中有一个 ‘%s’,message 函数不会按原样打印 ‘%s’,而是会查找字符串之后的参数。它会对第二个参数求值,并在字符串中 `’%s’` 所在的位置打印该值。

你可以通过将光标放在以下表达式后并输入 C-x C-e 来验证这一点:

1
(message "The name of this buffer is: %s." (buffer-name))

在 Info 中,回显区域会显示 “The name of this buffer is: info."。 buffer-name 函数返回缓冲区的名称作为字符串,message 函数将其插入 %s 的位置。

要将一个值以整数形式打印,可以像使用 %s 一样使用 %d。例如,要在回显区域中打印 `fill-column` 的值,可以对以下表达式求值:

1
(message "The value of fill-column is %d." fill-column)

在我的系统上,当我对这个列表求值时,回显区域会显示 “The value of fill-column is 72."。

如果引用的字符串中有多个 %s,第一个 %s 位置会打印引用字符串后第一个参数的值,第二个 %s 位置会打印第二个参数的值,依此类推。

例如,如果你对以下表达式求值:

1
2
(message "There are %d %s in the office!"
       (- fill-column 14) "pink elephants")

在你的回显区域中会出现一条相当异想天开的消息。在我的系统上,它显示为 “There are 58 pink elephants in the office!"。

表达式 (- fill-column 14) 会被求值,得到的数字将被插入到 %d 的位置;而双引号中的字符串 “pink elephants” 被视为一个单独的参数,并被插入到 %s 的位置。(也就是说,双引号之间的字符串求值为其自身,就像数字一样。)

最后,这是一个稍微复杂的例子,不仅说明了如何计算一个数字,还展示了如何在表达式中使用另一个表达式来生成替换 %s 的文本:

1
2
3
4
5
6
(message "He saw %d %s"
         (- fill-column 32)
         (concat "red "
                 (substring
                  "The quick brown foxes jumped." 16 21)
                 " leaping."))

在这个例子中,message 有三个参数:字符串 “He saw %d %s”,表达式 (- fill-column 32),以及以 concat 函数开头的表达式。 (- fill-column 32) 的求值结果被插入到 %d 的位置,而 concat 表达式的返回值则被插入到 %s 的位置。

当你的 fill-column 值为 70 并且你对这个表达式求值时,回显区域会显示消息 “He saw 38 red foxes leaping."。

设置变量的值

有几种方法可以为变量赋值。其中一种方法是使用特殊形式 setq。另一种方法是使用 let(参见 let)。在专业术语中,这个过程称为将变量绑定到一个值。

以下部分不仅描述了 setq 的工作原理,还说明了参数是如何传递的。

使用 setq

要将符号 flowers 的值设置为列表 (rose violet daisy buttercup),请通过将光标放在表达式后面并按下 C-x C-e 来对以下表达式求值:

1
(setq flowers '(rose violet daisy buttercup))

回显区域会显示列表 (rose violet daisy buttercup),这是 setq 特殊形式返回的内容。作为副作用,符号 flowers 被绑定到该列表;也就是说,符号 flowers,可以视为一个变量,被赋予了这个列表作为其值。(顺便说一下,这个过程说明了对 Lisp 解释器设置值的副作用可以是我们人类感兴趣的主要效果。这是因为每个 Lisp 函数必须返回一个值,除非它遇到错误,但它只有在被设计为具有副作用时才会有副作用。)

在对 setq 表达式求值后,你可以对符号 flowers 求值,它将返回你刚刚设置的值。以下是这个符号。将光标放在它后面并输入 C-x C-e。

1
flowers

当你对 flowers 求值时,回显区域会显示列表 (rose violet daisy buttercup)。

顺便提一下,如果你对带有前置单引号的变量 ‘flowers 求值,你将在回显区域看到的是符号本身,即 flowers。以下是带有引号的符号,你可以试试这个:

1
'flowers

另外,作为一种额外的方便,setq 允许你在一个表达式中为多个不同的变量设置不同的值。

要使用 setq 将变量 carnivores 的值设置为列表 ‘(lion tiger leopard),可以使用以下表达式:

1
(setq carnivores '(lion tiger leopard))

此外,setq 可以用来为不同的变量分配不同的值。第一个参数被绑定到第二个参数的值,第三个参数被绑定到第四个参数的值,依此类推。例如,你可以使用以下表达式将一组树的列表分配给符号 trees,并将一组食草动物的列表分配给符号 herbivores:

1
2
(setq trees '(pine fir oak maple)
      herbivores '(gazelle antelope zebra))

(这个表达式同样可以放在一行上,但可能不适合在一页上显示;而且人类更容易阅读格式整齐的列表。)

虽然我一直在使用“赋值”这个术语,但还有另一种理解 setq 工作方式的方式,那就是 setq 使符号指向列表。这种思维方式非常普遍,在后续章节中,我们将遇到至少一个名称中包含“指针”的符号。这个名称的选择是因为该符号有一个值,特别是一个列表,附加在它上面;或者换句话说,符号被设置为指向列表。

计数

下面是一个使用 setq 进行计数的示例。这可以用于计算你的程序某部分重复执行的次数。首先,将一个变量设置为零;然后每次程序重复执行时,将这个数字加一。要做到这一点,你需要一个变量作为计数器,以及两个表达式:一个初始的 setq 表达式将计数器变量设置为零;第二个 setq 表达式在每次被计算时增加计数器的值。

1
2
3
4
5
(setq counter 0)                ; 这是初始化器。

(setq counter (+ counter 1))    ; 这是增量器。

counter                         ; 这是计数器。

(‘;’ 后的文字是注释。详见“更改函数定义”部分。)

如果你计算第一个表达式,即初始化器 (setq counter 0),然后计算第三个表达式 counter,回显区域将显示数字 0。如果你接着计算第二个表达式,即增量器 (setq counter (+ counter 1)),计数器将获得值 1。因此,如果你再次计算 counter,回显区域将显示数字 1。每次你计算第二个表达式,计数器的值都会增加。

当你计算增量器 (setq counter (+ counter 1)) 时,Lisp 解释器首先计算最里面的列表,即加法。为了计算这个列表,它必须计算变量 counter 和数字 1。当它计算变量 counter 时,会获取其当前值。然后将这个值和数字 1 传递给 +,后者将它们相加。这个和作为内部列表的值返回,并传递给 setq,后者将变量 counter 设置为这个新值。因此,变量 counter 的值发生了变化。

总结

学习 Lisp 就像爬山,最开始的部分是最陡峭的。你现在已经攀登了最困难的部分,接下来随着你的进步会变得越来越容易。

总结如下:

  • Lisp 程序由表达式组成,表达式可以是列表或单个原子。
  • 列表由零个或多个原子或内嵌列表组成,这些元素之间用空格分隔,并由括号包围。列表可以为空。
  • 原子可以是多个字符组成的符号,如 forward-paragraph,也可以是单个字符的符号,如 +,或者是双引号括起来的字符串,或数字。
  • 数字的值就是它本身。
  • 双引号括起来的字符串的值也是它本身。
  • 当你单独计算一个符号时,会返回它的值。
  • 当你计算一个列表时,Lisp 解释器会查看列表中的第一个符号,然后查看绑定到该符号的函数定义。接着会执行函数定义中的指令。
  • 单引号 ’ 告诉 Lisp 解释器返回紧随其后的表达式本身,而不是像没有引号时那样进行计算。
  • 参数是传递给函数的信息。函数的参数是通过计算列表中除了第一个元素以外的其他元素得到的。
  • 函数在被计算时总是返回一个值(除非出现错误);此外,它还可能执行一些作为副作用的操作。在许多情况下,函数的主要目的是产生副作用。

练习

几个简单的练习:

  1. 通过计算一个不在括号内的适当符号生成一条错误信息。
  2. 通过计算一个在括号内的适当符号生成一条错误信息。
  3. 创建一个每次递增2而不是1的计数器。
  4. 编写一个在计算时在回显区域打印信息的表达式。

练习计算

在学习如何编写 Emacs Lisp 函数定义之前,花一些时间计算已经编写的各种表达式是很有用的。这些表达式将是以函数作为其第一个(通常也是唯一的)元素的列表。由于与缓冲区相关的一些函数既简单又有趣,我们将从这些函数开始。在本节中,我们将计算其中的一些函数。在另一节中,我们将研究其他几个与缓冲区相关的函数的代码,看看它们是如何编写的。

  • 如何求值
    每当你给 Emacs Lisp 一个编辑命令,例如移动光标或滚动屏幕的命令时,你都在对一个表达式求值,该表达式的第一个元素是一个函数。这就是 Emacs 的工作方式。

    当你输入按键时,你使 Lisp 解释器评估一个表达式,这就是你得到结果的方式。即使是输入普通文本也涉及评估一个 Emacs Lisp 函数,在这种情况下,这个函数使用 self-insert-command,它简单地插入你输入的字符。你通过按键输入来求值的函数被称为交互函数或命令;如何使一个函数变为交互式将在关于如何编写函数定义的章节中说明。参见“使一个函数变为交互式”。

    除了输入键盘命令,我们已经看到求值表达式的第二种方法:将光标定位在一个列表后面,然后按 C-x C-e。这是我们将在本节的其余部分中做的操作。还有其他对表达式求值的方法,我们将在遇到时描述它们。

    除了用于练习求值之外,接下来的几节中显示的函数本身也非常重要。学习这些函数可以清楚地区分缓冲区和文件、如何切换到缓冲区以及如何确定缓冲区中的位置。

缓冲区名称

buffer-name 和 buffer-file-name 这两个函数展示了文件和缓冲区之间的区别。当你求值以下表达式 (buffer-name) 时,缓冲区的名称会出现在回显区域。当你求值 (buffer-file-name) 时,缓冲区所指向的文件的名称会出现在回显区域。通常,(buffer-name) 返回的名称与它所指向的文件的名称相同,而 (buffer-file-name) 返回的是文件的完整路径名。

文件和缓冲区是两个不同的实体。文件是计算机中永久记录的信息(除非你删除它)。另一方面,缓冲区是 Emacs 中的信息,在编辑会话结束时(或当你杀死缓冲区时)会消失。通常,缓冲区包含你从文件中复制的信息;我们说缓冲区正在访问该文件。你所操作和修改的是这个副本。对缓冲区的更改不会更改文件,直到你保存缓冲区。当你保存缓冲区时,缓冲区会被复制到文件中,因此被永久保存。

如果你在 GNU Emacs 的 Info 模式中阅读此内容,你可以将光标放在每个表达式后面并按 C-x C-e 来对它们求值。

1
2
3
(buffer-name)

(Buffer-file-name)

当我在 Info 中执行此操作时,对 (buffer-name) 求值返回的值是 *info*,而对 (buffer-file-name) 求值返回的值是 nil。

另一方面,当我编写此文档时,对 (buffer-name) 求值返回的值是 “introduction.texinfo”,而对 (buffer-file-name) 求值返回的值是 “/gnu/work/intro/introduction.texinfo”。

前者是缓冲区的名称,后者是文件的名称。在 Info 中,缓冲区名称是 *info*。Info 没有指向任何文件,所以对 (buffer-file-name) 求值的结果是 nil。符号 nil 源自拉丁语,意为“无”;在这种情况下,它意味着该缓冲区没有关联到任何文件。(在 Lisp 中,nil 也用于表示“假”,并且是空列表 () 的同义词。)

当我在写作时,我的缓冲区名称是 “introduction.texinfo”。它所指向的文件名称是 “/gnu/work/intro/introduction.texinfo”。

(在表达式中,括号告诉 Lisp 解释器将 buffer-name 和 buffer-file-name 作为函数处理;如果没有括号,解释器会尝试将这些符号作为变量进行求值。参见“变量”。)

尽管文件和缓冲区之间存在区别,你常常会发现人们在指代缓冲区时说成文件,反之亦然。实际上,大多数人会说“我在编辑一个文件”,而不是说“我在编辑一个即将保存到文件的缓冲区”。从上下文中几乎总能清楚地知道人们的意思。然而,当处理计算机程序时,记住这种区别是很重要的,因为计算机不像人那么聪明。

顺便提一下,“缓冲区”一词源自其作为缓冲垫的含义,可以减弱碰撞的冲击力。在早期的计算机中,缓冲区缓冲了文件与计算机中央处理单元之间的交互。存放文件的磁鼓或磁带和中央处理单元是两种截然不同的设备,它们以各自的速度间歇地工作。缓冲区使它们能够有效地协同工作。最终,缓冲区从一个中介、一个临时存储地,发展成为进行工作的地方。这种转变有点像一个小港口发展成一个大城市:起初它只是一个临时储存货物并等待装船的地方,后来它成为了一个商业和文化中心。

并非所有缓冲区都与文件相关联。例如,*scratch* 缓冲区不访问任何文件。同样,*Help* 缓冲区也不与任何文件关联。

在早期,如果你没有 ~/.emacs 文件并通过只输入 emacs 命令(而不是指定任何文件)启动一个 Emacs 会话, Emacs 会以 scratch 缓冲区可见的状态启动。如今,你会看到一个启动画面。你可以按照启动画面上建议的命令之一操作,访问一个文件,或者按 q 退出启动画面并进入 scratch 缓冲区。

如果你切换到 scratch 缓冲区,输入 (buffer-name),将光标放在它后面,然后按 C-x C-e 来对表达式求值。返回的名称 scratch 将出现在回显区域中。*scratch* 是缓冲区的名称。当你在 scratch 缓冲区中输入 (buffer-file-name)`并进行求值时,nil 将会出现在回显区域中,就像你在 Info 中求值 (buffer-file-name) 时一样。

顺便说一下,如果你在 scratch 缓冲区中,希望表达式返回的值出现在 scratch 缓冲区本身而不是回显区域中,可以输入 C-u C-x C-e 而不是 C-x C-e。这会使返回的值出现在表达式之后。缓冲区看起来会像这样:

1
(buffer-name)"*Scratch*"

你不能在 Info 中这样做,因为 Info 是只读的,它不允许你更改缓冲区的内容。但是,你可以在任何可以编辑的缓冲区中这样做;当你编写代码或文档(例如本书)时,这个功能非常有用。

获取缓冲区

buffer-name 函数返回缓冲区的名称;要获取缓冲区本身,则需要使用另一个函数:current-buffer。如果你在代码中使用这个函数,你将得到的是缓冲区本身。

名称和名称所指的对象或实体是不同的。你不是你的名字,你是一个被别人用名字称呼的人。如果你请求与乔治交谈,而有人递给你一张写有字母“G”、“e”、“o”、“r”、“g”和“e”的卡片,你可能会觉得好笑,但不会满意。你想交谈的不是名字,而是名字所指的人。缓冲区也是类似的:*scratch* 是 scratch 缓冲区的名称,但名称不是缓冲区本身。要获取缓冲区本身,你需要使用像 current-buffer 这样的函数。

然而,这里有一个小小的复杂之处:如果你在表达式中单独对 current-buffer 求值(如我们在这里将做的那样),你看到的将是缓冲区名称的打印表示形式,而不是缓冲区的内容。 Emacs 之所以这样工作,有两个原因:缓冲区可能有成千上万行长,太长而不便于显示;并且另一个缓冲区可能具有相同的内容但名称不同,因此区分它们是很重要的。

以下是包含该函数的一个表达式:

1
(current-buffer)

如果你在 Emacs 的 Info 中以通常方式对这个表达式求值,#<buffer *info*> 将会出现在回显区域。这种特殊格式表明返回的是缓冲区本身,而不仅仅是其名称。

顺便提一下,虽然你可以在程序中输入数字或符号,但你不能输入缓冲区的打印表示形式:获取缓冲区本身的唯一方法是使用像 current-buffer 这样的函数。

一个相关的函数是 other-buffer。它返回最近选择的、当前缓冲区之外的另一个缓冲区,而不是它名称的打印表示形式。如果你最近在 scratch 缓冲区之间来回切换,other-buffer 将返回那个缓冲区。

你可以通过对以下表达式求值看到这一点:

1
(other-buffer)

你应该会看到 #<buffer *scratch*> 出现在回显区域中,或者看到你最近切换回的其他缓冲区的名称。

切换缓冲区

other-buffer 函数在用作需要缓冲区作为参数的函数的参数时,实际上提供了一个缓冲区。我们可以通过使用 other-buffer 和 switch-to-buffer 来切换到不同的缓冲区,看到这一点。

但首先,简要介绍一下 switch-to-buffer 函数。当你在 Info 和 scratch 缓冲区之间来回切换以求值 (buffer-name) 时,你很可能按下了 C-x b,然后在 minibuffer 中提示你输入要切换到的缓冲区名称时输入了 *scratch*。这些按键 C-x b 会使 Lisp 解释器评估交互函数 switch-to-buffer。正如我们之前所说,这就是 Emacs 的工作方式:不同的按键调用或运行不同的函数。例如,C-f 调用 forward-char,M-e 调用 `forward-sentence`,等等。

通过在表达式中写出 switch-to-buffer 并提供一个要切换到的缓冲区,我们可以像 C-x b 那样切换缓冲区:

1
(switch-to-buffer (other-buffer))

switch-to-buffer 符号是列表的第一个元素,因此 Lisp 解释器会将其视为一个函数,并执行与之相关的指令。但在此之前,解释器会注意到 other-buffer 在括号内,并先对该符号进行处理。 other-buffer 是该列表的第一个(在此情况下也是唯一的)元素,因此 Lisp 解释器会调用或运行该函数,它将返回另一个缓冲区。接下来,解释器运行 switch-to-buffer,并将返回的另一个缓冲区作为参数传递给它,这就是 Emacs 将要切换到的缓冲区。如果你在 Info 中阅读此内容,现在就试试。对这个表达式求值。(要返回原缓冲区,请键入 C-x b RET。)

在本文件后续章节的编程示例中,你会更常看到 set-buffer 函数,而不是 switch-to-buffer。这是因为计算机程序和人类之间存在差异:人类有眼睛,期望在计算机终端上看到自己正在处理的缓冲区。这显而易见,几乎不言自明。然而,程序没有眼睛。当计算机程序处理缓冲区时,该缓冲区不需要在屏幕上可见。

switch-to-buffer 是为人类设计的,它做了两件事:它切换 Emacs 的注意力指向的缓冲区,并在窗口中将显示的缓冲区切换到新缓冲区。而 set-buffer 只做一件事:它将计算机程序的注意力切换到另一个缓冲区。屏幕上的缓冲区保持不变(当然,通常在命令运行完成之前那里不会发生任何变化)。

另外,我们刚刚引入了另一个术语,即“调用”(call)。当你评估一个列表时,其中的第一个符号是一个函数,你就是在调用那个函数。这个术语的使用源于将函数视为一个可以为你做某事的实体——就像水管工是一个你可以打电话给他或她修理漏水的实体一样。

缓冲区大小和点的位置

最后,让我们来看几个相对简单的函数:buffer-size、point、point-min 和 point-max。这些函数提供了有关缓冲区大小以及光标在其中位置的信息。

buffer-size 函数告诉你当前缓冲区的大小;即,该函数返回缓冲区中字符的数量。

1
(buffer-size)

你可以通过将光标定位在表达式之后并按 C-x C-e 的通常方式来评估它。

在 Emacs 中,光标的当前位置称为“点”(point)。表达式 (point) 返回一个数字,表示从缓冲区开头到光标所在位置的字符数量。

你可以通过以通常方式评估以下表达式,查看当前缓冲区中点的字符计数:

1
(point)

在我写下这些内容时,point 的值是 65724。point 函数在本书后面的一些示例中经常使用。

point 的值当然取决于它在缓冲区中的位置。如果你在这个位置对 point 求值,那么该数值会更大:

1
(point)

对我来说,point 在此位置的值是 66043,这意味着两个表达式之间有 319 个字符(包括空格)。(毫无疑问,你会看到不同的数字,因为在我第一次 point 求值之后,我已经对内容进行了编辑。)

point-min 函数与 point 类似,但它返回当前缓冲区中 point 的最小允许值。这个值通常是 1,除非启用了“缩小”功能。(缩小是一种机制,通过它你可以限制自己或程序只在缓冲区的一部分进行操作。详见“缩小和扩展”部分。)同样,point-max 函数返回当前缓冲区中 point 的最大允许值。

练习

找到一个你正在处理的文件,并移至其中间位置。找出该文件的缓冲区名称、文件名、长度以及你在文件中的位置。

如何编写函数定义

当 Lisp 解释器对一个列表进行求值时,它会查看该列表的第一个符号是否附有一个函数定义;换句话说,即该符号是否指向一个函数定义。如果是,计算机就会执行定义中的指令。一个附有函数定义的符号简单地被称为“函数”(虽然严格来说,函数是定义本身,而符号是对它的引用)。

  • 关于原始函数的说明
    所有函数都是用其他函数定义的,除了少数用C编程语言编写的原始函数。当你编写函数的定义时,你会在Emacs Lisp中编写它们,并使用其他函数作为构建块。你使用的一些函数本身会用Emacs Lisp编写(可能是你自己写的),而有些则是用C编写的原始函数。原始函数的用法和用Emacs Lisp编写的函数完全相同,并且表现也相似。它们之所以用C编写,是为了使GNU Emacs能够在任何具有足够性能并能运行C语言的计算机上轻松运行。

    让我再强调一下这一点:当你用Emacs Lisp编写代码时,你不会区分使用C语言编写的函数和使用Emacs Lisp编写的函数。区别是无关紧要的。我之所以提到这个区别,只是因为知道它是很有趣的。实际上,除非你深入研究,否则你不会知道一个已编写的函数是用Emacs Lisp编写的还是用C编写的。

defun 宏

在 Lisp 中,像 mark-whole-buffer 这样的符号有与之关联的代码,用来告诉计算机在调用该函数时该做什么。这段代码称为函数定义,它是通过符号 defun(“define function” 的缩写)开头的 Lisp 表达式创建的。

在接下来的章节中,我们将查看来自 Emacs 源代码的函数定义,例如 mark-whole-buffer。在本节中,我们将描述一个简单的函数定义,以便你了解它的样子。这个函数定义使用了算术运算,因为这使得例子更简单。一些人不喜欢使用算术运算的例子;然而,如果你是这样的人,不必担心。在本介绍的其余部分,我们研究的代码几乎不涉及算术或数学。这些例子大多以某种方式涉及文本。

一个函数定义在 defun 之后最多有五个部分:

  1. 该函数定义应附加到的符号名称。
  2. 将传递给函数的参数列表。如果没有参数将传递给函数,这就是一个空列表 ()。
  3. 描述该函数的文档。(技术上是可选的,但强烈推荐。)
  4. 可选地,一个表达式,使函数可交互,这样你可以通过输入 M-x 然后函数名称,或者按下适当的键或组合键来使用它。
  5. 指示计算机该怎么做的代码:函数定义的主体。

将函数定义的五个部分想象成一个模板,其中每个部分都有一个插槽,这样会更有帮助。

1
2
3
4
(defun function-name (arguments)
  "optional-documentation…"
  (interactive argument-passing-info)     ;可选的
  body)

作为示例,以下是一个将其参数乘以 7 的函数代码。(这个例子不是交互式的。关于如何使函数变得交互式,请参见“使函数变得交互式”的相关信息。)

1
2
3
(defun multiply-by-seven (number)
  "Multiply NUMBER by seven."
  (* 7 number))

这个定义以一个括号和符号 defun 开头,接着是函数的名称。

函数名称之后是一个包含将传递给函数的参数的列表,这个列表称为参数列表。在这个例子中,列表只有一个元素:符号 number。当函数被使用时,这个符号将绑定到作为函数参数的值。

作为参数的名称,我本可以选择其他名字,而不是用 number 这个词。例如,我可以选择 multiplicand(被乘数)。我选择“number”这个词是因为它表明了这个位置的值应该是什么类型的;但我同样可以选择“multiplicand”这个词,来表示放在这个位置的值在函数工作中的作用。我甚至可以叫它 foogle,但那会是个糟糕的选择,因为它不会告诉人们它的含义。参数名称的选择取决于程序员,应该选择一个能使函数含义明确的名字。

实际上,你可以为参数列表中的符号选择任何名称,甚至是某个其他函数中使用的符号名称:你在参数列表中使用的名称是该定义所私有的。在该定义中,这个名称指的是不同于函数定义外部任何使用相同名称的实体。假设你在家人中有一个昵称叫“Shorty”;当你的家人提到“Shorty”时,他们指的是你。但在你的家人之外,例如在电影中,“Shorty”这个名字可能指的是其他人。由于参数列表中的名称对于函数定义来说是私有的,你可以在函数体内更改该符号的值,而不会更改其在函数外部的值。其效果类似于 let 表达式产生的效果。(参见 let。)

参数列表之后是描述函数的文档字符串。这是当你输入 C-h f 和函数名称时看到的内容。顺便提一下,当你编写这样的文档字符串时,应该使第一行成为一个完整的句子,因为有些命令(如 apropos)只打印多行文档字符串的第一行。此外,如果文档字符串有第二行,你不应该缩进它,因为在使用 C-h f (describe-function) 时,这样会显得不协调。虽然文档字符串是可选的,但它非常有用,几乎应该包含在你编写的每一个函数中。

例子的第三行是函数定义的主体。(当然,大多数函数的定义比这个要长得多。)在这个函数中,主体是列表 (* 7 number),它表示将 number 的值乘以 7。(在 Emacs Lisp 中,* 是乘法函数,就像 + 是加法函数一样。)

当你使用 multiply-by-seven 函数时,参数 number 将被求值为你想要使用的实际数字。下面是一个展示如何使用 multiply-by-seven 的例子;不过,暂时不要尝试对其求值!

1
(multiply-by-seven 3)

在实际使用该函数时,在下一节的函数定义中指定的符号 number 被绑定到值 3。请注意,虽然在函数定义中 number 在括号内,但传递给 multiply-by-seven 函数的参数不是在括号内。括号在函数定义中被写出是为了让计算机能识别参数列表的结束位置和函数定义其余部分的开始位置。

如果你对这个例子求值,你很可能会得到一个错误消息。(不妨试试看!)这是因为我们已经编写了函数定义,但还没有告诉计算机这个定义——我们还没有在 Emacs 中加载函数定义。安装函数的过程就是告诉 Lisp 解释器函数的定义。安装的过程将在下一节中介绍。

安装函数定义

如果你正在 Emacs 的 Info 模式中阅读本文,可以通过先对函数定义求值,然后再对 (multiply-by-seven 3) 求值来试用 multiply-by-seven 函数。以下是函数定义的副本。将光标放在函数定义的最后一个括号之后,然后按 C-x C-e。这样做时,multiply-by-seven 会出现在回显区域。(这意味着,当对函数定义求值时,它返回的值是被定义的函数的名称。)同时,这个操作会安装函数定义。

1
2
3
(defun multiply-by-seven (number)
  "Multiply NUMBER by seven."
  (* 7 number))

通过对这个 defun 求值,你刚刚在 Emacs 中安装了 multiply-by-seven。现在,这个函数和 forward-word 或任何其他你使用的编辑函数一样,已经成为 Emacs 的一部分了。(multiply-by-seven 将一直保持安装状态,直到你退出 Emacs。要了解如何在每次启动 Emacs 时自动重新加载代码,请参阅“永久安装代码”。)

安装的效果

你可以通过对以下示例求值来查看安装 multiply-by-seven 的效果。将光标放在以下表达式之后,然后按 C-x C-e。数字 21 将出现在回显区域。

1
(Multiply-by-seven 3)

如果你愿意,可以通过输入 C-h f (describe-function),然后输入函数名 multiply-by-seven 来查看该函数的文档。这样做时,屏幕上会出现一个 Help 窗口,上面显示:

1
2
3
4
5
multiply-by-seven is a Lisp function.

(multiply-by-seven NUMBER)

Multiply NUMBER by seven.

(要返回屏幕上的单窗口显示,输入 C-x 1。)

修改函数定义

如果你想更改 multiply-by-seven 的代码,只需重写它即可。要用新版本替换旧版本,只需再次对函数定义求值。这就是在 Emacs 中修改代码的方式,非常简单。

例如,你可以将 multiply-by-seven 函数更改为将数字自身相加七次,而不是将数字乘以七。这样做会通过不同的方式产生相同的答案。同时,我们还会在代码中添加一条注释;注释是 Lisp 解释器忽略的文本,但可能对人类读者有用或有启发。注释指出这是第二个版本。

1
2
3
(defun multiply-by-seven (number)       ; 第二个版本。
  "Multiply NUMBER by seven."
  (+ number number number number number number number))

注释位于分号 ; 之后。在 Lisp 中,分号后面的一行内容都是注释。行尾就是注释的结束。如果要将注释扩展到两行或更多行,每行都要以分号开头。

关于注释的更多信息,请参见《GNU Emacs Lisp参考手册》中的“编写 .emacs 文件”和“注释”部分。

你可以通过对这个版本的 multiply-by-seven 函数求值来安装它,方法与对第一个函数求值的方式相同:将光标放在最后一个括号之后,然后按 C-x C-e。

总之,这就是在 Emacs Lisp 中编写代码的方法:编写一个函数,安装它,测试它,然后进行修复或改进并再次安装。

使函数具有交互性

你可以通过在文档之后紧接着放置一个以特殊形式 interactive 开头的列表来使一个函数具有交互性。用户可以通过键入 M-x 然后输入函数名称来调用一个交互函数;或者通过键入与其绑定的快捷键来调用,例如,键入 C-n 调用 next-line 或 C-x h 调用 mark-whole-buffer。

有趣的是,当你以交互方式调用一个交互函数时,返回的值不会自动显示在回显区域。这是因为你通常调用一个交互函数是为了它的副作用,比如向前移动一个单词或一行,而不是为了它的返回值。如果每次按键时返回的值都显示在回显区域,那将会非常分散注意力。

交互式的 multiply-by-seven:概述

通过创建一个交互版本的 multiply-by-seven 函数,可以说明特殊形式 interactive 的使用方法,以及如何在回显区显示一个值。

代码如下:

1
2
3
4
(defun multiply-by-seven (number)       ; 交互版本。
  "将 NUMBER 乘以七。"
  (interactive "p")
  (message "结果是 %d" (* 7 number)))

您可以通过将光标放置在代码之后并输入 C-x C-e 来安装此代码。函数名称将显示在回显区中。然后,您可以输入 C-u 和一个数字,再输入 M-x multiply-by-seven 并按下 RET 键来使用该代码。短语 “The result is …”(结果是…)以及相应的乘积将出现在回显区中。

更一般地说,您可以通过以下两种方式调用这样的函数:

  1. 输入包含要传递数字的前缀参数,然后输入 M-x 和函数名,例如 C-u 3 M-x forward-sentence
  2. 输入函数绑定的按键或按键组合,例如 C-u 3 M-e。

以上两个示例的效果相同,都是将光标向前移动三个句子。(由于 multiply-by-seven 没有绑定到按键,因此不能用作按键绑定的示例。)

(请参阅 “按键绑定”,了解如何将命令绑定到按键。)

可以通过按下 META 键然后输入一个数字(例如 M-3 M-e),或者按下 C-u 然后输入一个数字(例如 C-u 3 M-e),将前缀参数传递给交互式函数(如果只输入 C-u 而没有数字,则默认值为 4)。

一个交互的乘以七函数

让我们看看特殊形式 interactive 的用法,然后看一下 multiply-by-seven 函数的交互版本中的 message 函数。你可能记得函数定义如下所示:

1
2
3
4
(defun multiply-by-seven (number)       ; 交互版本。
  "将 NUMBER 乘以七。"
  (interactive "p")
  (message "结果是 %d" (* 7 number)))

在这个函数中,表达式 (interactive “p”) 是一个包含两个元素的列表。“p” 告诉 Emacs 将前缀参数传递给函数,并将其值用于函数的参数。

这个参数将是一个数字。这意味着符号 number 将在以下这一行中绑定到一个数字:

1
(message "结果是 %d" (* 7 number))

例如,如果你的前缀参数是 5,Lisp 解释器将像下面这样计算这一行:

1
(message "结果是 %d" (* 7 5))

(如果你在 GNU Emacs 中阅读此内容,可以自己计算这个表达式。)首先,解释器将计算内部列表,即 (* 7 5)。这会返回一个值 35。接着,它将计算外部列表,把列表的第二个及后续元素的值传递给 message 函数。

正如我们所见,message 是一个 Emacs Lisp 函数,专为向用户发送单行消息而设计。(请参见 message 函数。)总的来说,message 函数会将其第一个参数按原样打印在回显区,除了 %d 或 %s 的出现(以及其他我们尚未提到的各种 % 开头的序列)。当它遇到一个控制序列时,函数会查看第二个或后续参数,并在字符串中控制序列所在的位置打印该参数的值。

在交互的 multiply-by-seven 函数中,控制字符串是 %d,它要求一个数字,而通过计算 (* 7 5) 返回的值是数字 35。因此,数字 35 会被打印在 %d 的位置,消息内容为 “The result is 35”。

(注意,当你调用 multiply-by-seven 函数时,消息是直接打印的,没有引号;但当你调用 message 函数时,文本会以双引号打印。这是因为当你计算一个其第一个元素是 message 的表达式时,message 返回的值会显示在回显区;但当 message 嵌入在函数中时,它作为一个副作用打印文本而不带引号。)

interactive 的不同选项

在示例中,multiply-by-seven 使用 “p” 作为 interactive 的参数。这个参数告诉 Emacs 将你输入的 C-u 后跟一个数字,或 META 后跟一个数字解释为将该数字作为参数传递给函数的命令。 Emacs 预定义了超过二十个字符用于 interactive。在几乎每种情况下,这些选项中的一个将使你能够以交互方式向函数传递正确的信息。(请参阅《GNU Emacs Lisp 参考手册》中的 “Code Characters for interactive”。)

考虑函数 zap-to-char。它的 interactive 表达式是:

1
(interactive "p\ncZap to char: ")

interactive 参数的第一部分是你已经熟悉的 p。该参数告诉 Emacs 将前缀解释为要传递给函数的数字。你可以通过输入 C-u 后跟一个数字或输入 META 后跟一个数字来指定前缀。前缀是指定字符的数量。因此,如果你的前缀是 3,指定的字符是 x,那么你将删除直到第三个 x(包括它)之前的所有文本。如果不设置前缀,则删除直到指定字符(包括它)之前的所有文本。

c 告诉函数要删除的字符的名称。

更正式地说,一个具有两个或多个参数的函数可以通过将部分添加到 interactive 后面的字符串中,将信息传递给每个参数。当你这样做时,信息按照在 interactive 列表中指定的顺序传递给每个参数。在字符串中,每个部分由 \n(换行符)分隔开。例如,你可以在 p 后面加上一个 \n 和 cZap to char: 。这会使 Emacs 传递前缀参数的值(如果有)和字符。

在这种情况下,函数定义如下所示,其中 arg 和 char 是 interactive 绑定前缀参数和指定字符的符号:

1
2
3
4
5
(defun name-of-function (arg char)
  "文档……"
  (interactive "p\ncZap to char: ")
  函数主体……
)

(在提示符中的冒号后加上空格,使提示看起来更好。参见 copy-to-buffer 的定义示例。)

当一个函数不需要参数时,interactive 也不需要参数。这样的函数包含一个简单的表达式 (interactive)。 mark-whole-buffer 函数就是这样的例子。

另外,如果这些特殊的字母代码不适合你的应用,你可以将自己的参数作为列表传递给 interactive。

参见 append-to-buffer 的定义示例。有关此技术的更完整解释,请参阅《GNU Emacs Lisp 参考手册》中的 “Using Interactive”。

永久安装代码

当你通过计算函数定义来安装它时,该函数将一直保持安装状态,直到你退出 Emacs。下次你启动一个新的 Emacs 会话时,该函数将不会被安装,除非你再次计算函数定义。

在某个时候,你可能希望每次启动 Emacs 新会话时自动安装代码。你可以通过以下几种方法实现:

  • 如果你有仅供自己使用的代码,可以将函数定义的代码放在 .emacs 初始化文件中。当你启动 Emacs 时,.emacs 文件会自动计算,文件中的所有函数定义都会被安装。参见 “Your .emacs File”。
  • 或者,你可以将需要安装的函数定义放在一个或多个文件中,并使用 load 函数让 Emacs 计算这些文件,从而安装其中的每个函数。参见 “Loading Files”。
  • 第三,如果你有供整个站点使用的代码,通常将其放在一个名为 site-init.el 的文件中,该文件在构建 Emacs 时被加载。这使得所有使用你的机器的人都可以使用这些代码。(参见 Emacs 发行版中的 INSTALL 文件。)

最后,如果你有代码希望所有 Emacs 用户都能使用,你可以将其发布在计算机网络上,或将副本发送给自由软件基金会。(当你这样做时,请将代码及其文档在允许他人运行、复制、研究、修改和再分发代码的许可证下发布,并保护自己不被剥夺你的工作成果。)如果你将代码副本发送给自由软件基金会,并正确保护自己和他人,这些代码可能会被包含在 Emacs 的下一个版本中。在很大程度上,Emacs 在过去的几年中就是通过这样的捐赠逐步扩展的。

let

let 表达式是 Lisp 中的一种特殊形式,在大多数函数定义中你都需要使用它。

let 用于将一个符号附加或绑定到一个值上,这样 Lisp 解释器就不会将变量与另一个不属于该函数的同名变量混淆。

要理解为什么需要 let 这种特殊形式,可以考虑你拥有一所房子的情况,你通常称之为“the house”,例如在这句话中:“The house needs painting.” 如果你在拜访朋友时,你的主人提到“the house”,他很可能指的是他自己的房子,而不是你的,也就是说,是指另一所房子。

如果你的朋友指的是他自己的房子,而你以为他指的是你的房子,这就可能引发混淆。在 Lisp 中也可能发生同样的情况:如果一个函数内部使用的变量与另一个函数内部使用的变量同名,而这两个变量并不打算表示相同的值,那么可能会导致混淆。let 特殊形式可以防止这种混淆。

let 防止混淆

let 特殊形式可以防止混淆。let 为局部变量创建一个名称,遮蔽在 let 表达式之外对相同名称的任何使用(在计算机科学术语中,这称为“绑定变量”)。这就像理解在你主人的家中,每当他提到“the house”时,他指的是他的房子,而不是你的房子。(用于命名函数参数的符号也以完全相同的方式绑定为局部变量。参见 defun 宏。)

另一种理解 let 的方式是,它在你的代码中定义了一个特殊的区域:在 let 表达式的主体内部,你命名的变量有其自己的局部意义。在 let 主体之外,它们有其他的意义(或者可能根本没有定义)。这意味着在 let 主体内部,为 let 表达式命名的变量调用 setq 会设置该名称的局部变量的值。然而,在 let 主体之外(例如,在调用另一个地方定义的函数时),为 let 表达式命名的变量调用 setq 不会影响该局部变量。

let 可以一次创建多个变量。同时,let 为其创建的每个变量赋予一个初始值,该初始值可以是你指定的值或 nil。(在术语中,这称为将变量绑定到一个值。)在 let 创建并绑定了变量之后,它会执行 let 主体中的代码,并将主体中最后一个表达式的值作为整个 let 表达式的值返回。(“执行”是一个术语,意味着计算一个列表;它来自该词的含义“付诸实际效果”(《牛津英语词典》)。因为你计算一个表达式来执行一个动作,“执行”已经演变为“计算”的同义词。)

comments powered by Disqus
你若安好,便是晴天
使用 Hugo 构建
主题 StackJimmy 设计