🏆 我提供关于 Emacs、Linux 和生活方面的私人课程:https://protesilaos.com/coach/价格实惠

Emacs Lisp Elements

Emacs Lisp 编程语言的宏观视角

这本由 Protesilaos Stavrou,也被称为 "Prot" 撰写的书,提供了对 Emacs Lisp 编程语言的宏观视角。 本文提供的信息对应于 1.0.0 稳定版本,发布于 2025-04-12。

目录

1. Emacs Lisp 入门

本书的目的是为您提供 Emacs Lisp (也称为 "Elisp") 的宏观视角。这是您用来扩展 Emacs 的编程语言。Emacs 是一个可编程的文本编辑器:它解释 Emacs Lisp 并相应地执行。您可以使用 Emacs 而无需编写任何代码:它已经有很多功能。但是,您可以随时对其进行编程,通过计算一些您自己编写或从其他人那里获得 Elisp 代码(例如,以 package 的形式)来完全按照您想要的方式执行。

编程您自己的文本编辑器既有用又有趣。例如,您可以通过将您不断重复的一系列操作组合成一个命令,然后将其分配给一个键绑定来简化它们:键入该键 - boom! - 一次完成所有中间任务。这可以提高您的效率,同时将编辑器变成一个舒适的工作环境。

有趣的部分是您如何编写代码。您无需遵守任何义务。没有! 您为了编程而编程。这是一种娱乐活动,可以扩展您的视野。另外,您可以培养您的 Elisp 技能,如果您选择修改 Emacs 的某些行为,这些技能将来可能会很有用。

修改 Emacs 是体验的一部分。它教会您对编辑器的运作方式毫不客气地持有自己的观点。关键是要掌握足够的 Elisp,这样您就不会花费太多时间玩乐或因某些琐碎的事情无法正常工作而感到沮丧。我本人也是一个修补匠,没有任何计算机科学或邻近研究的背景:我通过反复试验来学习 Emacs Lisp,通过玩弄编辑器。我的名义目标是改善我一遍又一遍地重复的某些微动:我寻求效率,却发现了一些更深刻的东西。学习扩展我的编辑器是一种充实的体验,因此我提高了工作效率。Emacs 会按照我想要的方式执行,我对它感到满意。

本文的每一章通常都很简短明了。有些对初学者比较友好,而另一些则深入探讨高级主题。各章节之间存在链接,就像参考手册应该做的那样。然后您可以来回查找所需内容。

您将在此处找到的文本是散文和代码的组合。后者可能是实际的 Elisp 或伪代码,可以捕获底层模式。我鼓励您在 Emacs 内部或随时使用 Emacs 阅读本书。这样,您可以玩弄我提供给您的函数,以进一步了解它们的细微之处。

我采用的“宏观视角”方法旨在涵盖我在使用 Emacs Lisp 时经常遇到的概念。本书不能替代 Emacs Lisp Reference Manual,绝不应将其视为我对任何 Elisp 形式的评论的真实来源。

祝你好运,玩得开心!

2. 计算 Emacs Lisp

您在 Emacs 中所做的一切都会调用某些函数。它计算 Emacs Lisp 代码,读取返回值并产生副作用(副作用和返回值)。

您在键盘上键入一个键,并且一个字符被写入当前 buffer。这是绑定到该键的函数。实际上,它是一个 interactive 函数,因为您是通过键绑定而不是通过某些程序来调用它的。Interactive 函数被称为“commands”。但是,不要让交互性的实现细节分散您对这样一个事实的注意力,即您在 Emacs 中执行的每个操作都涉及 Emacs Lisp 的计算。

另一种常见的交互模式是使用 M-x (execute-extended-command) 键,默认情况下它运行 execute-extended-command 命令:它生成一个 minibuffer 提示,要求您按名称选择一个命令并继续执行它。

Emacs 可以从任何地方计算 Elisp 代码。如果您的 buffer 中有一些 Elisp 代码,则可以将光标放在其右括号的末尾,然后键入 C-x C-e (eval-last-sexp)。同样,您可以使用命令 eval-buffereval-region 分别对当前 buffer 或突出显示的区域进行操作。

eval-last-sexp 也可以在 symbols 上使用(Symbols, 平衡表达式和 Quoting)。例如,如果将光标放在变量 buffer-file-name 的末尾并使用 C-x C-e (eval-last-sexp),您将获得该变量的值,该值是 nil 或您正在编辑的文件的文件系统路径。

有时以上这些方法不适合您尝试执行的操作。假设您打算编写一个命令来复制当前 buffer 的文件路径。为此,您需要您的代码来测试变量 buffer-file-name 的值(Buffers 作为数据结构)。但是您不想在实际文件中键入 buffer-file-name,然后使用上述 Elisp 计算命令之一,然后再撤消您的编辑。这既繁琐又容易出错!在当前 buffer 中运行 Elisp 的最佳方法是键入 M-: (eval-expression):它打开 minibuffer 并期望您编写要计算的代码。从那里键入 RET 继续。计算是使用最后一个 buffer 作为 current(调用 eval-expression 之前的 buffer)完成的。

以下是一些 Emacs Lisp 代码,您可能想在 (i) 对应于文件的 buffer 与 (ii) 未与磁盘上的任何文件关联的 buffer 中尝试。

;; 使用 `eval-expression' 在文件访问 buffer 与未访问任何文件的 buffer 中计算此代码。
(if buffer-file-name
  (message "此文件的路径是 `%s'" buffer-file-name)
 (message "对不起,哥们,此 buffer 未访问文件"))

当您试验代码时,您想测试它的行为方式。使用命令 ielm 打开一个 interactive shell。它会把您放到一个提示符下,您可以在那里键入任何 Elisp 代码并按 RET 来计算它。返回值将打印在正下方。或者,切换到 *scratch* buffer。如果它使用 major mode lisp-interaction-mode,这是变量 initial-major-mode 的默认值,那么您可以在该 buffer 中自由移动并在某些代码的末尾键入 C-j (eval-print-last-sexp) 来计算它。这与 eval-last-sexp 的工作方式几乎相同,但增加的效果是将返回值放在您刚刚计算的表达式的正下方。

除了这些之外,您还可以依靠 Emacs 的自我记录性质来弄清楚当前的状态。例如,要了解变量 major-mode 的 buffer-local 值,您可以执行 C-h v (describe-variable),然后搜索该变量。生成的帮助 buffer 将通知您 major-mode 的当前值。此帮助命令和许多其他命令(如 describe-functiondescribe-keymapdescribe-keydescribe-symbol)可以深入了解 Emacs 对给定对象的了解。帮助 buffer 将显示相关信息,例如定义给定函数的文件路径或变量是否声明为 buffer-local。

Emacs 是“自我记录”的,因为它报告其状态。您不需要显式更新帮助 buffers。这是通过计算相关代码自动发生的:Emacs 有效地向您显示您正在处理的任何对象的最新值。

3. 副作用和返回值

Emacs Lisp 具有函数。它们接受输入并产生输出。在其最纯粹的形式中,函数是一种仅返回值的计算:它不会更改其环境中的任何内容。函数的返回值用作另一个函数的输入,这实际上是一个计算链。因此,您可以依靠函数的返回值来表达诸如“如果此操作有效,则也执行另一件事,否则执行其他操作甚至什么也不做”之类的意思。

Elisp 是一种扩展和控制 Emacs 的语言。这意味着它也会影响编辑器的状态。当您运行一个函数时,它可以进行永久性的更改,例如在光标处插入一些文本、删除一个 buffer、创建一个新窗口等等。这些更改将对未来的函数调用产生影响。例如,如果前一个函数删除了某个 buffer,则下一个应该写入该 buffer 的函数将无法再执行其工作:该 buffer 已消失!

当您编写 Elisp 代码时,您必须同时考虑返回值和副作用。如果您很马虎,您将获得由所有那些考虑不周的环境更改引起的意外结果。但是,如果您一丝不苟地使用副作用,您就有能力将 Elisp 发挥到其全部潜力。例如,假设您定义一个函数,该函数遵循以下逻辑:“创建一个 buffer,转到那里,写入一些文本,将该 buffer 保存到我首选位置的文件中,然后返回到调用此函数之前的位置,同时保持创建的 buffer 处于打开状态。”所有这些都是副作用,并且它们都有用。您的函数也可能有一些有意义的返回值,您可以将其用作另一个函数的输入。例如,您的函数将返回它生成的 buffer 对象,以便下一个函数可以在那里做一些事情,例如在单独的 frame 中显示该 buffer 并使其文本更大。

这个想法是操纵编辑器的状态,使 Emacs 按照您的设想执行。有时这意味着您的代码具有副作用。在其他时候,副作用是无用的,甚至与您想要的结果背道而驰。随着您获得更多经验并扩展您的技能范围(Symbols, 平衡表达式和 Quoting),您将不断完善您对需要做什么的直觉。没问题;没有压力!

4. Buffers 作为数据结构

Buffer 将数据保存为字符序列。例如,此数据是您打开文件时看到的文本。每个字符都存在于给定的位置,这是一个数字。函数 point 为您提供您所在位置的 position,通常对应于光标所在的位置(计算 Emacs Lisp)。在 buffer 的开头,point 返回值 1副作用和返回值)。有很多函数返回 buffer position,例如 point-minpoint-maxline-beginning-positionre-search-forward。其中一些函数会产生副作用,例如 re-search-forward 会将光标移动到给定的 match 位置。

当您使用 Emacs Lisp 进行编程时,您经常依靠 buffers 来执行以下某些操作:

提取文件内容作为字符串 将 buffer 视为一个大的字符串。您可以使用函数 buffer-string 将其全部内容作为一个潜在的巨大字符串获取。您也可以获取两个 buffer position 之间的子字符串,例如使用 buffer-substring 函数或其 buffer-substring-no-properties 对应项(文本的属性)。想象一下,您这样做是更广泛操作的一部分,该操作 (i) 打开一个文件,(ii) 转到某个 position,(iii) 复制它找到的文本,(iv) 切换到另一个 buffer,以及 (v) 将其找到的内容写入这个新 buffer。

展示某些操作的结果 您可能有一个函数显示即将到来的假期。您的代码在后台进行计算,最终将一些文本写入 buffer。最终产品正在显示。根据您执行此操作的方式,您将需要计算函数 get-buffer-create 或其更严格的 get-buffer 替代项。如果您需要清除现有 buffer 的内容,您可以使用 with-current-buffer macro 临时切换到您要定位的 buffer,然后调用函数 erase-buffer 删除所有内容,或者使用 delete-region 将删除限制在两个 buffer position 之间的范围内。最后,函数 display-bufferpop-to-buffer 将把 buffer 放置在 Emacs 窗口中。

将变量与给定的 buffer 相关联 在 Emacs Lisp 中,变量可以采用与其全局对应变量不同的 buffer-local 值。有些变量甚至被声明为始终是 buffer-local 的,例如 buffer-file-namefill-columndefault-directory。假设您正在执行类似于返回访问给定目录中的文件的 buffers 的列表的操作。您将遍历 buffer-list 函数的返回值,以通过测试 buffer-file-name 的某个值来相应地过滤结果(使用 ifcond 等的基本控制流)。这个特定的变量始终可用,但是您始终可以使用 setq-local macro 将值分配给当前 buffer 中的变量。

后一点可能是最开放的。Buffers 就像一个变量包,其中包括它们的内容、它们正在运行的 major mode 以及它们拥有的所有 buffer-local 值。在以下代码块中,我使用 seq-filter 函数来遍历函数 buffer-list 的返回值(Symbols, 平衡表达式和 Quoting)。

(seq-filter
 (lambda (buffer)
  "如果 BUFFER 可见且其 major mode 派生自 `text-mode',则返回 BUFFER。"
  (with-current-buffer buffer
   ;; 不打算让用户看到的 buffers 的约定是以空格开头命名。我们现在对这些不感兴趣。
   (and (not (string-prefix-p " " (buffer-name buffer)))
     (derived-mode-p 'text-mode))))
 (buffer-list))

这将返回一个 buffer 对象列表,这些对象通过了 (i) 对用户“可见”和 (ii) 它们的 major mode 是 text-mode 或从中派生的测试。以上也可以这样编写(何时使用命名函数或 Lambda 函数):

(defun my-buffer-visble-and-text-p (buffer)
 "如果 BUFFER 可见且其 major mode 派生自 `text-mode',则返回 BUFFER。"
 (with-current-buffer buffer
  ;; 不打算让用户看到的 buffers 的约定是以空格开头命名。我们现在对这些不感兴趣。
  (and (not (string-prefix-p " " (buffer-name buffer)))
     (derived-mode-p 'text-mode))))
(seq-filter #'my-buffer-visble-and-text-p (buffer-list))

与 buffers 一样,Emacs 窗口和 frames 具有自己的参数。我不会介绍这些参数,因为它们的实用性更加专业化,并且概念是相同的。只需知道它们是您可以用来发挥优势的数据结构,包括通过遍历它们(遍历 List 中的元素)。

5. 文本的属性

就像 buffers 充当数据结构一样(Buffers 作为数据结构),任何文本也可以具有与之关联的属性。这是您使用 Emacs Lisp 检查的元数据。例如,当您在某些编程 buffer 中看到语法高亮显示时,这是文本属性的效果。某些函数负责“propertise”或“fontify”相关文本,并决定将其应用于称为“face”的对象。Faces 是将排版和颜色属性(例如字体系列和粗细以及前景和背景色调)捆绑在一起的构造。要获取一个带有光标所在位置的文本属性信息的帮助 buffer,请键入 M-x (execute-extended-command),然后调用命令 describe-char。它会告诉您它看到的字符、它的渲染字体、它的代码点以及它的文本属性。

假设您正在编写自己的 major mode。在实验的早期阶段,您希望手动将文本属性添加到 major mode 为 fundamental-mode 的 buffer 中的所有 I have properties 短语的实例,因此您会执行类似以下的操作(上次搜索的 Match Data):

(defun my-add-properties ()
 "将属性添加到当前 buffer 中跨文本 \"I have properties\" 的文本。"
 (goto-char (point-min))
 (while (re-search-forward "I have properties" nil t)
  (add-text-properties (match-beginning 0) (match-end 0) '(face error))))

实际上尝试一下。使用 C-x b (switch-to-buffer),键入一些与现有 buffer 不匹配的随机字符,然后按 RET 以访问该新 buffer。它运行 fundamental-mode,这意味着没有发生任何“fontification”,因此,my-add-properties 将按预期工作。现在粘贴以下内容:

This is some sample text. Will the phrase "I have properties" use the `bold' face?
What does it even mean for I have properties to be bold?

继续使用 M-: (eval-expression) 并调用函数 my-add-properties。它有效吗?它应用的 face 称为 error。忽略该词的语义:我选择它只是因为它通常以一种相当强烈和明显的方式进行样式设置(尽管您当前的主题可能会以不同的方式进行)。

有一些函数可以查找给定 buffer 位置的属性,还有一些函数可以向前和向后搜索给定的属性。具体细节现在并不重要。我希望您记住的是,文本不仅仅是构成它的字符。有关更多详细信息,请键入 M-x (execute-extended-command) 以调用命令 shortdoc。它会要求您提供一个文档组。选择 text-properties 以了解更多信息。好吧,对那里列出的所有内容都使用 shortdoc。我一直这样做。

6. Symbols, 平衡表达式和 Quoting

对于不熟悉 Emacs Lisp 的人来说,这是一种有很多括号的语言!这是一个简单的函数定义:

(defun my-greet-person (name)
 "向 NAME 的人打招呼。"
 (message "你好 %s" name))

我刚刚定义了名为 my-greet-person 的函数。它有一个参数列表,特别是一个名为 name 的参数列表。然后是可选的文档字符串,供用户理解代码和/或理解函数的意图。my-greet-person 接受 name 并将其作为参数传递给函数 message,以最终打印问候语。message 函数将文本记录在 *Messages* buffer 中,您可以使用 C-h e (view-echo-area-messages) 直接访问该 buffer。无论如何,这就是您使用它期望的一个参数来调用 my-greet-person 的方式:

(my-greet-person "Protesilaos")

现在使用多个参数执行相同的操作:

(defun my-greet-person-from-country (name country)
 "向居住在 COUNTRY 的 NAME 打招呼。"
 (message "你好,来自 %s 的 %s" name country))

并这样调用它:

(my-greet-person-from-country "Protesilaos" "Cyprus")

即使是最基本的任务,您也有很多括号。但是不要害怕!这些实际上使您可以更轻松地对代码进行结构化理解。如果现在感觉不是这样,那是因为您还不习惯它。一旦您习惯了,就不会再回头了。

任何 Lisp 方言(Emacs Lisp 是其中之一)的基本思想是您具有分隔列表的括号。列表由 elements 组成。列表被计算以产生某些计算结果,或者按原样返回以用于其他计算(副作用和返回值):

列表作为函数调用 当计算一个列表时,第一个 element 是函数名称,其余 elements 是传递给它的参数。您已经看到了以上我如何使用 "Protesilaos" 作为其参数来调用 my-greet-person 的方式。对于 my-greet-person-from-country 也是相同的原理,其中 "Protesilaos""Cyprus" 作为其参数。

列表作为数据 当一个列表未被计算时,那么它的任何 element 在一开始都没有任何特殊的含义。它们都作为列表返回,而没有进一步的更改。当您不希望计算您的列表时,请在其前面加上单引号字符。例如,'("Protesilaos" "Prot" "Cyprus") 是一个包含三个 element 的列表,应按原样返回。

考虑您尚未见过的后一种情况。您有一个 elements 的列表,并且您想从中获取一些数据。在最基本的层面上,函数 carcdr 分别返回第一个 element 和所有剩余 elements 的列表:

(car '("Protesilaos" "Prot" "Cyprus"))
;; => "Protesilaos"
(cdr '("Protesilaos" "Prot" "Cyprus"))
;; => ("Prot" "Cyprus")

此处的单引号至关重要,因为它指示 Emacs 不要计算该列表。否则,计算此列表会将第一个 element(即 "Protesilaos")视为函数名称,并将列表的其余部分视为该函数的参数。由于您没有此类函数的定义,因此会出现错误。

Emacs Lisp 中的某些数据类型是“self-evaluating”。这意味着如果您计算它们,它们的返回值就是您已经看到的。例如,字符串 "Protesilaos" 的返回值是 "Protesilaos"。对于字符串、数字、keywords、symbols 以及特殊的 nilt 都是如此。这是一个包含每个示例的列表,您可以通过调用函数 list 来构造该列表:

(list "Protesilaos" 1 :hello 'my-greet-person-from-country nil t)
;; => ("Protesilaos" 1 :hello 'my-greet-person-from-country nil t)

list 函数计算传递给它的参数,除非它们被 quoted。您获得返回值而没有任何明显更改的原因是因为 self-evaluation。请注意,my-greet-person-from-country 的 quoted 方式与我们 quote 一个我们不希望计算的列表的方式相同。如果没有它,my-greet-person-from-country 将被计算,除非它也被定义为一个变量,否则这将返回一个错误。

将单引号视为明确的指令:“不要计算以下内容。” 更具体地说,它是在该上下文中通常会发生计算的情况下,不执行计算的指令(List 中的部分计算)。换句话说,您不想在 quoted 列表中 quote 某些内容,因为这与 quote 它两次相同:

;; 这是正确的方法:
'(1 :hello my-greet-person-from-country)
;; Quote `my-greet-person-from-country' 是错误的,因为无论如何都不会计算整个列表。这里的错误是您 quote 已经 quoted 的内容,比如 ''my-greet-person-from-country。
'(1 :hello 'my-greet-person-from-country)

现在您可能想知道为什么我们 quote 了 my-greet-person-from-country 而没有 quote 其他任何东西?原因是您在那里看到的所有其他内容实际上都是“self-quoting”,即 self-evaluation 的反面。而 my-greet-person-from-country 是一个 symbol。一个“symbol”是对自身以外事物的引用:它要么代表某些计算(一个函数),要么代表一个变量的值。如果您在不 quote 它的情况下编写一个 symbol,您实际上是在告诉 Emacs“给我这个 symbol 代表的值”。在 my-greet-person-from-country 的情况下,如果您尝试这样做,您会得到一个错误,因为这个 symbol 不是一个变量,因此尝试从中获取一个值将不起作用。

请记住,Emacs Lisp 有一个“macro”的概念,它基本上是一个模板系统,用于编写实际上扩展到其他代码然后进行计算的代码。在一个 macro 内部,您可以控制 quote 的方式,这意味着上述内容可能不适用于涉及 macro 的调用,即使它们仍然在 macro 的扩展形式中使用(Macro 或特殊形式中的计算)。

当您接触到更多的 Emacs Lisp 代码时,您会遇到前面带有井号的 quotes,比如 #'some-symbol。这个被称为“sharp quote”的东西与常规的 quote 相同,只是增加了特别引用函数的语义。因此,程序员可以更好地表达给定表达式的意图,而字节编译器可以在内部执行必要的检查和优化。在这种情况下,请阅读分别对应于 quote 和 sharp quote 的函数 quotefunction

7. List 中的部分计算

您已经了解了 Emacs Lisp 代码的外观(Symbols, 平衡表达式和 Quoting)。您有一个列表,该列表要么被计算,要么被按原样获取。还有一种情况是应该部分计算一个列表,或者更具体地说,应该将其视为数据而不是函数调用,其中其中包含的某些 elements 仍然需要进行计算。

在以下代码块中,我定义了一个名为 my-greeting-in-greek 的变量,这是希腊语中的一个常用短语,字面意思是“祝您健康”,发音为“yah sou”。为什么是希腊语?好吧,您已经获得了引发整个 Lisp 业务的 lambda,因此您不妨获得其余的(何时使用命名函数或 Lambda 函数)!

(defvar my-greeting-in-greek "Γεια σου"
 "希腊语的基本问候语,祝愿某人健康。")

现在我想试验 message 函数,以更好地了解计算的工作原理。让我从 quote 列表的场景开始,从而将其按原样获取:

(message "%S" '(one two my-greeting-in-greek four))
;;=> "(one two my-greeting-in-greek four)"

您会注意到变量 my-greeting-in-greek 没有被计算。我得到了 symbol,即实际的 my-greeting-in-greek,但没有得到它代表的值。这是预期的结果,因为整个列表都被 quote 了,因此,其中的所有内容都不会被计算。

现在,检查下一个代码块,以了解如何告诉 Emacs,我希望整个列表仍然被 quote,但是特别是要计算 my-greeting-in-greek,因此将其替换为它的值:

(message "%S" `(one two ,my-greeting-in-greek four))
;; => "(one two \"Γεια σου\" four)"

请密切注意此处的语法。我没有使用单引号,而是使用了反引号,在我们的例子中,反引号也被称为“quasi quote”。它的行为类似于单引号,只是以逗号开头的任何内容除外。逗号是“计算后面的内容”的指令,并且仅在 quasi-quoted 列表内有效。后面的“thing”可以是 symbol 或列表。当然,列表可以是函数调用。然后让我使用 concat 来问候某人,同时将所有内容作为列表返回:

(message "%S" `(one two ,(concat my-greeting-in-greek " " "Πρωτεσίλαε") four))
;; => "(one two \"Γεια σου Πρωτεσίλαε\" four)"

请记住,如果您根本不 quote 此列表,则会收到一个错误,因为第一个 element one 将被视为一个函数 symbol,该函数将使用所有其他 elements 作为其参数进行调用。one 很可能未在您当前的 Emacs 会话中定义为函数,或者这些参数对它没有任何意义。另外,twofour 将被视为变量,因为它们未被 quote,在这种情况下,也必须定义这些变量,否则将出现更多错误。

除了逗号运算符之外,还有 ,@(甚至如何发音?也许是“comma at”?),它是“splicing”的符号。这是用于表示“返回值是一个列表,我希望您删除它最外面的括号”的术语。实际上,通常会返回 '(one two three) 的代码现在返回 one two three。这种差异在真空中可能没有多大意义,但是一旦您将这些 elements 视为应该以自己的权利起作用的表达式,而不是简单地成为 quoted 列表的 elements,它就会起作用。我将在此处不详细说明一个示例,因为我认为最好在定义 macros 的上下文中介绍它(Macro 或特殊形式中的计算)。

您很可能不需要使用部分计算的知识。它在 macros 中更常见,但是可以应用于任何地方。无论如何都要注意它,因为在某些情况下,您至少会想了解您所依赖的某些代码正在做什么。

最后,自从我向您介绍了一些希腊语单词以来,我现在认为您是我的朋友。这是我小时候的一个笑话。我试图向我的英语老师解释一些事情。由于我缺乏表达自己的词汇,我开始使用希腊语单词。我的老师有一个只回答英语的严格政策,所以她说“It is all Greek to me.”不知道她的回答是“我不理解你”的习语,我轻率地回答说,“Yes, Greek madame; me no speak England very best.”当时我实际上并不是初学者,但是我不会错过嘲笑这种情况的机会。就像您应该记住享受花在修补 Emacs 上的时间一样。但够了!回到阅读本书。

8. Macro 或特殊形式中的计算

在 Emacs Lisp 代码最基本的情况下,您具有计算或不计算的列表(Symbols, 平衡表达式和 Quoting)。如果稍微花哨一点,则具有仅部分计算的列表(List 中的部分计算)。但是有时,您会看一段代码,并且无法理解为什么 quote 和计算的常规规则不适用。在您实际看到它之前,请检查一个典型的函数调用,该调用还涉及变量的计算:

(concat my-greeting-in-greek " " "Πρωτεσίλαε")

您在关于部分计算的部分中遇到了这段代码。您在这里要做的是调用函数 concat,然后是三个参数。其中一个参数是一个变量,即 my-greeting-in-greek。当计算此列表时,Emacs 实际做的是首先计算参数(包括 my-greeting-in-greek),以便获得它们各自的值,然后才使用这些值调用 concat。您可以将整个操作视为如下:

换句话说,以下两个产生相同的结果(假设一个常量 my-greeting-in-greek):

(concat my-greeting-in-greek " " "Πρωτεσί