跳转到内容

Emacs/Emacs Lisp 入门

来自 Wikibooks,开放世界中的开放书籍

Emacs Lisp 是一种编程语言,属于 Lisp 语言家族(包括 Scheme 和 Common Lisp)。Lisp 是目前仍在使用的第二古老的编程语言(仅次于 Fortran),但尽管 Lisp 社区仍然活跃,但现在规模非常小。因此,大多数开发人员从未有理由学习 Lisp,并且许多使用 Emacs 的开发人员将 Emacs Lisp 视为外星领地。

尽管 Emacs Lisp 不是主流编程语言,但它是一种强大的语言,用于实现 Emacs 本身的大部分功能。这意味着作为 Emacs 扩展作者,您使用的是与最初用于编写编辑器的相同的语言,并可以访问所有相同的库。这使得 Emacs 具有独特的强大功能;虽然其他编辑器(如 Eclipse)接受修改其行为的扩展,但没有其他编辑器可以在运行时使用原生代码调整其行为。

Emacs Lisp 的简单示例

[编辑 | 编辑源代码]

Emacs Lisp 是 Emacs 思维方式中如此基础的一部分,以至于它不会让您去某个特殊的地方执行 Lisp 表达式;您可以在任何缓冲区中执行它。在编写 C 函数的过程中,您可以直接在其中插入一些 Emacs Lisp,执行它,并立即看到结果。

进入任何 Emacs 缓冲区并键入以下内容

(+ 1 2)

将光标定位在右括号之后,然后键入C-x C-e(按住 Ctrl 键并按 x,然后按住 Ctrl 键并按 e)。表达式将被求值,结果将显示在迷你缓冲区中。此表达式只是将值 1 和 2 相加,因此结果值 3 应该出现在迷你缓冲区中。

虽然计算很简单,但它的表达方式可能会让一些人感到困惑。Emacs Lisp 中的表达式始终采用相同的形式:左括号、函数标识符、该函数的参数列表,最后是右括号。上面所有的表达式意味着我们正在调用+函数(加法)并为其提供 1 和 2 的参数。

这种写操作的方式被称为波兰表示法(也称为波兰前缀表示法或简称前缀表示法)。对于大多数程序员来说,看到加法以这种方式编写一开始会觉得不自然,因为他们习惯于看到用运算符位于中间的数学表达式,即所谓的中缀表示法。但是,大多数程序员也熟悉调用函数,其中函数名称出现在参数之前。大多数编程语言都区分运算符(使用中缀表示法)和函数(使用前缀表示法)。在 Lisp 家族的语言中,没有这种区别,所有调用都使用前缀表示法。虽然这需要一些时间来适应,但它带来的好处是所有代码都遵循相同的结构,这使得 Lisp 代码更容易阅读和编写 Lisp 代码,从而使语言更擅长自省。

如果这让你感到困扰,值得提醒自己函数和运算符之间的分界线是多么随意。特别是在允许重新定义或专门用于类的面向对象语言中,它们实际上只是对函数调用的语法糖。

函数调用中可以有任意数量的参数(假设函数支持它,大多数函数在这样做有意义的情况下都会支持)。

(+ 1 2 3 4)

函数的参数本身可以是函数调用。例如,以下表达式将前三个平方数相加

(+ (* 1 1) (* 2 2) (* 3 3))

左括号后的第一个元素必须始终是函数标识符。

括号是表达式不可分割的一部分;如果从(+ 1 2)中删除括号,则那里没有 Lisp 表达式(准确地说,有三个单独且不相关的 Lisp 表达式,没有函数求值)。额外的括号并不像在某些语言中那样无害。考虑表达式

((+ 1 2 3))

表达式是递归求值的,因此内部表达式首先被求值,这将简化为

(6)

此表达式的含义是应用没有参数的函数6,但由于没有这样的函数,因此会抛出错误。您可以将括号视为类似于 C 中函数调用的括号,而不是像简单的算术括号一样用于修改运算符优先级。

事实上,Lisp 运算符表示法的优点之一是运算符的求值方式永远不会有任何歧义,因此永远不需要额外的括号来消除歧义。例如,使用中缀表示法,表达式5 * 4 + 3是可能的,这需要读者了解优先级规则才能知道它将如何被求值((5 * 4) + 35 * (4 + 3))。在 Lisp 语言中,等效的表达式将被写成(+(* 5 4) 3),因此永远不会有任何歧义。

与 Emacs Lisp 交互

[编辑 | 编辑源代码]

虽然您可以从 Emacs 中的任何缓冲区执行 Emacs Lisp 语句,但最方便的做法是为该目的保留一个缓冲区,以避免弄乱包含重要工作的缓冲区。当您启动 Emacs 时,它会为您创建一个特殊的缓冲区*scratch*,该缓冲区与文件无关(除非您稍后决定保存其内容)。这使得它成为编写和执行 Emacs Lisp 语句的良好选择。

*scratch*缓冲区对于 Lisp 执行还有另一个优势,即默认情况下,它以 Lisp 交互模式启动。在此模式下,您可以使用C-j执行任何 Emacs Lisp 表达式,结果将永久插入缓冲区中,而不是暂时显示在迷你缓冲区中。您可以使用M-x lisp-interaction-mode将任何缓冲区置于 Lisp 交互模式。

定义函数

[编辑 | 编辑源代码]

正如您所料,您可以在 Emacs Lisp 中定义自己的函数,它们的工作方式与内置函数相同。您可以使用defun表达式定义函数

(defun my-add (x y)
   (+ x y))

尝试将此内容键入 emacs 并求值该表达式。Emacs 应该回复

my-add

定义函数的返回值只是函数本身,在本例中为my-add。这说明了一个重要观点:每个 Lisp 表达式都有一个值。这类似于 C 中函数的返回值,只是您不必显式返回任何内容。Emacs 只会获取函数体中的最后一个表达式并将其视为返回值。Emacs Lisp 中没有void函数,尽管调用者可以随意忽略任何不感兴趣的返回值。

定义了新函数后,您现在就可以像使用内置函数一样使用它了

(my-add 1 2)

这给出了预期的结果3。请注意,您不能像使用内置函数(+)一样,向此加法函数传递任意数量的参数。别担心,创建可以以这种方式工作的函数是完全可能的,我们稍后将学习如何做到这一点。

代码和数据

[编辑 | 编辑源代码]

任何 Lisp 中最简单的的数据结构是列表;实际上,列表赋予了 Lisp 编程语言其名称,它是列表处理的缩写。这个名字也许有点用词不当,因为 Lisp 可以用于比列表丰富得多的数据结构,但无处不在的列表为探索 Lisp 编程提供了自然的起点。

将以下内容键入 Lisp 求值缓冲区并执行它(如果您处于 lisp 交互模式,则为C-j,否则为C-x C-e

(list 1 2 3)

Emacs 将回复

(1 2 3)

列表可以包含任意数量的项目(或零个项目),并且这些项目不必都具有相同的类型。事实上,列表可以包含其他列表作为条目,形成嵌套的数据结构

(list 1 2 "buckle my shoe" (list 3 4))

如果您对它进行求值,Emacs 将回复

(1 2 "buckle my shoe" (3 4))

当您输入表达式时,Emacs 会对其进行求值并将结果返回给您。表达式(list 1 2 3)使用三个参数对list函数进行求值。调用list函数的结果是一个列表,它将被返回给您:(1 2 3)。那么,如果(1 2 3)是列表的正确语法,为什么您必须调用list函数,而不是直接键入列表呢?尝试在 Emacs 中执行以下 Lisp 表达式

(1 2 3)

Emacs 将显示错误消息[1]

Lisp error: (invalid-function 1)

这里的问题是,通过输入列表并要求 Emacs 对其进行求值,您要求将其视为 Lisp 代码,而不是数据。Lisp 代码由一个或多个项目列表组成,其中每个列表中的第一个项目必须是函数标识符,列表中其余的项目是函数的参数(它们本身可以是需要求值的表达式)。由于您尝试求值表达式(1 2 3),因此 Emacs 假设1必须是函数标识符,当它找不到这样的函数时,它会抛出一个错误。(list 1 2 3)不会出现同样的问题,因为list是一个函数;这是代码,而不是数据。

上面一段话中有一个重要的点需要重复强调:Lisp 代码仅仅是 Lisp 数据,所有 Lisp 数据都可以被视为代码。这也许是 Lisp 与所有其他主流编程语言的关键区别。它使得 Lisp 代码在运行时生成更多 Lisp 代码变得相对容易。这个简单的设计决策一举使语言的丰富性呈指数级增长,因为在其他语言中需要特殊语言支持的高阶功能,只需使用普通的 Lisp 代码即可编写。

如果 Lisp 代码和数据之间唯一的区别在于代码会被求值而数据不会,那么我们必须有一种方法来告诉 Lisp 特定的数据是否需要被求值。正如我们上面看到的,一种方法是使用像 list 这样的简单函数,它只是返回它的参数。这是一种解决问题的方法,但不是一种优雅的方法。通过使用 list,我们并没有成功地阻止求值发生,我们只是写了一个求值很简单的表达式。更严重的是,对于嵌套列表,您必须修改每个嵌套列表的级别(例如 (list 1 2 (list 3 4)),而不仅仅是最外层)。

由于 Lisp 的默认行为是求值列表,因此我们只需要某种方法来阻止求值即可控制求值。这是通过 quote 运算符完成的,它像任何其他 Lisp 函数调用一样编写,但会导致其参数不被求值。quote 只接受一个参数,但参数可以是一个列表,当然也包括嵌套的子列表。尝试评估以下内容

(quote (1 2))
(quote (1 (2 3) 4))
(quote 1 2)

第三种形式会抛出一个错误,因为 quote 只允许一个参数。第二种形式表明子表达式 (1 2) 没有被求值,即使正常的求值是依次求值每个参数。

嵌套表达式的行为表明了一些重要的事情:quote 不是一个普通的 Lisp 函数。如果您尝试自己实现 quote,您会发现这是不可能的,因为 quote 的参数会在您的自定义函数被调用之前就被求值。由于错误(在上面的例子中求值 (1 2))会在您的函数被调用之前抛出,因此您的代码无法从中恢复。

quote 不是 Lisp 函数,而是一小部分特殊形式之一。特殊形式与 Lisp 函数具有相同的语法,但具有 Lisp 解释器提供的普通函数无法提供的特殊行为。

您可能想知道为什么可以求值某些表达式而不能求值其他表达式。例如,以下所有表达式都可以被求值,即使它们都不包含函数

12
"twelve"
nil
:thing

即使这些表达式都没有被引用,它们也都求值为自身。这是因为 Lisp 将某些值视为自求值,这意味着如果将它们作为 Lisp 表达式求值,它们将返回它们自己的值。这适用于整数和字符串,并且通常在没有歧义的情况下适用。它不适用于列表,因为普通代码是用列表的语法表达的,因此如果列表求值为自身,则无法求值任何代码。

由于 quote 预计会被频繁使用,因此有一个方便的语法糖 - 一个撇号字符 '

(quote (1 2))
'(1 2)

这两个表达式都求值为相同的 (1 2) 列表。


变量和作用域

[编辑 | 编辑源代码]

与一些基于函数式范式的编程语言不同,Emacs Lisp 具有大多数开发人员都非常熟悉的可变变量。Emacs Lisp 中的变量是无类型的,这意味着一个变量可以保存您想赋予它的任何值:数字、字符串,甚至函数都可以赋值给变量。同一个变量可以在一个点保存一个整数,而在几行代码之后保存一个函数定义(尽管大多数开发人员会认识到这不是好的做法)。

在 Emacs Lisp 中使用变量最简单的方法是使用全局变量。顾名思义,这些变量可以在程序的任何地方读取和写入,并且永久保留其值。

在 Emacs Lisp 中使用全局变量时需要注意的一件事是,没有命名空间的概念可以用来将您库中对全局变量的引用与其他人库或 Emacs 核心中的全局变量分开。因此,最好以特定于您库的字符串作为前缀来命名您的全局变量。

全局变量在 Emacs(以及第三方 Emacs Lisp 包)中被广泛用于保存模块的简单配置设置。虽然无节制地使用全局变量会使代码难以理解,但如果谨慎使用,它提供了一种以最小的开销来控制配置的方法。用于配置的全局变量通常会与其相关联的文档。您可以通过将光标移动到相关变量上并键入 C-h v 来访问此文档。

您可以使用 setq 特殊形式为现有变量赋值或创建新变量


Clipboard

待办事项
如果我们还没有解释什么是特殊形式,那么这里应该给出一些进一步的解释。


(setq some-variable 12)

setq 形式将其第二个参数的值赋给第一个参数中给出的变量。

  • 作用域变量
  • let 和 let*
  • 缓冲区局部变量

函数作为一等对象

[编辑 | 编辑源代码]
  • 将函数作为参数传递
  • Lambda 函数
  • lambda 和 defun 之间的比较

编写函数的函数

[编辑 | 编辑源代码]
  • define-skeleton 作为示例
  1. 实际上,Emacs 会为您提供完整的回溯信息,以显示问题发生的位置;为了清楚起见,这里省略了详细信息,但不要对您的错误消息看起来比这里描述的更复杂感到惊讶
华夏公益教科书