Introduction to Emacs Lisp

我们将涵盖什么?

这是一个实用系列!

  • Emacs Lisp 基础知识
  • 关于函数和变量的全部内容
  • 操纵 Emacs
  • 处理系统
  • 创建和使用可扩展性点
  • 编写宏
  • 发布到MELPA

我们将至少以一个新包为例进行工作!

这个系列面向谁?

  • 不仅仅是面向程序员!
  • 每个人都应该有机会享受 Emacs hack

如果您对 Lisp 很熟悉,不要担心如果我跳过某些概念!

Lisp 是什么?

  • 那门语言有大量令人讨厌的多余括号
  • 一门基于交互性理念的语言和环境
  • 语法可以定义新的语言结构
  • 最终的黑客语言!

Emacs Lisp

  • Emacs 配置和扩展的 Lisp 方言
  • Emacs 内置的大部分功能都是用它编写的!
  • 具有专注于发现性和可扩展性的核心特性
  • 用于许多 Emacs 专有类型(缓冲区、窗口等)的API和数据类型

我们将在此视频中关注核心概念!

Lisp 语法

Lisp 的魅力来自于其语法的简单性!

Lisp 语法主要由列表(lists)、符号(Symbols)和值(values)组成。

(defun the-meaning-of-life (answer)
  (message "The answer is %s" answer))

;; 列表中的换行符和空格可以随处添加
(list 1 2 3
      4 5 6
      7 8 9)

代码也可以被视为数据!

您不应害怕编辑带有括号的代码,因为有相关包可以解决这个问题:)

值 Values (or “Objects”)

任何值或对象都有一个类型。它还有一种文本表达式,这种表示方式可能是“可读的”,也可能不是“可读的”!

Emacs Lisp 有许多内置类型

  • 数值:整数、浮点数
  • 字符串:在双引号中
  • 符号:以冒号开头
  • 列表:以括号表示,包含其他 Emacs Lisp 对象
  • 向量:类似列表但使用方括号
  • 布尔值:t 和 nil
  • 函数:可以调用的对象
  • 宏:用来生成代码的函数
  • 关键字:以冒号开头的符号,用作参数

例如:

1: 42      ; 整数(数值)
2: "Hello" ; 字符串
3: :symbol ; 符号
4: (1 2 3) ; 列表
5: [1 2 3] ; 向量
6: t       ; 真值(布尔值)
7: nil     ; 假值(布尔值)
8: (lambda () (message "Hello!")) ; 函数

并非所有这些值都具有“可读”的文本表示。例如,函数和宏的实现细节通常隐藏在模糊表示之下。这样可以抽象语义,而不是表示方式。

然而,Emacs Lisp 还提供了一种机制,允许您查看任何对象的底层表示 - 这就是`macroexpand-all`。使用它,您可以查看函数、宏和其他对象的“展开”表示。

所以虽然不是所有的 Emacs Lisp 值都可读,但语言本身确实提供了方法来理解它们的表示和内部构造。这使其成为一门非常透明的语言!

总之,理解 Emacs Lisp 中的类型和值是掌握语言的重要一步。我希望这个概述能给您一个很好的起点。请随时提出任何问题!

见:Programming-Types

Emacs Types

Emacs Lisp 还有许多专用于 Emacs 的类型,其中大多数类型没有代码表达式:

  • 缓冲区:文件/文本的内存表示
  • 窗口:Emacs 屏幕上的视图
  • 布局:窗口的排列方式
  • 键绑定:键与命令的映射
  • 面板:特殊的显示区域
  • 监视器:用于跟踪变量/缓冲区的工具
  • 日志缓冲区:用于消息/日志的特殊缓冲区
  • Frames
  • Threads
  • 等等

使用这些 Emacs 专用类型,我们可以影响 Emacs 界面:

;; Get the previous buffer and switch to it
(switch-to-buffer (other-buffer))

见:Editing-Types

例如:

  • 我们可以创建、删除、重命名和修改缓冲区来打开/关闭文件并编辑文本。
  • 我们可以创建、删除和排列窗口来组织我们的屏幕布局。
  • 我们可以定义键绑定来为键盘快捷键分配命令。
  • 我们可以使用面板显示定制数据和接口。
  • 我们可以使用日志缓冲区在 Emacs 中显示消息和其他信息。

等等。

所以虽然我们无法直接访问这些类型的底层表示,但我们可以充分利用它们在 Emacs Lisp 层公开的接口来构建界面和用户体验。

举个例子,考虑一个包,它在启动时打开4个窗口:

  • 一个编辑文件
  • 一个显示文档
  • 一个运行REPL
  • 一个显示日志消息

它还可以:

  • 为一些键绑定命令来导航这些窗口
  • 在日志窗口中打印状态消息
  • 当文件保存时刷新文档窗口

等等。

所以尽管这个包无法访问窗口、缓冲区、面板和键绑定的内部构造,它仍然可以构建一个定制的多窗口界面与复杂的用户交互。

这些类型使用 Emacs C核心库定义和实现。 Emacs Lisp 层只暴露它们的接口,以供配置和扩展 Emacs。

举个例子,缓冲区是一种非常重要的类型,用于表示打开的文件或文本。但是在 Emacs Lisp 中,一个缓冲区只是一个对象 - 您可以获取其属性,将其作为参数传递给函数,等等。实际的数据和行为是在 Emacs C层实现的。

所以对于这些类型,我们通常只能使用和操纵它们在 Emacs Lisp 层公开的接口。我们无法访问或更改它们的底层表示。

这为 Emacs Lisp 带来了一定的抽象,同时也限制了我们对 Emacs 核心的访问。但是,公开的接口通常已经足够丰富,可以实现非常强大和复杂的自定义功能!

所以不要太担心这些类型缺乏代码表示 - 您可以通过 Emacs Lisp 层与它们进行充分交互,这就足够了。如果确实需要更底层的访问,则需要编写C代码和 Emacs 模块来扩展核心。

Forms 和运算

“Form”是任何可以求值的 lisp 对象。

评估的工作原理

当您在 Emacs 中按下 Enter 键或调用 eval-defun 等命令时,就会发生评估。

评估的工作原理如下:

  1. Emacs 会找出当前的“表格” - 这可能是:
    • 您刚刚输入的表达式
    • 函数内的表达式
    • 让我们假设它是 (+ 1 2)
  2. Emacs 会调用 Lisp 求值器来对表格求值。它会检查表格的第一个元素, 在这种情况下是符号 +
  3. Emacs查找 + 的定义,并调用它。由于 + 是一个内置函数,所以它执行加法运算。
  4. + 函数对其参数 12 进行求值,得到两个数值。
  5. + 函数将这些数值相加,得出结果 3
  6. 这个结果 3 成为整个表格 (+ 1 2) 的值。它被“返回”给调用方。
  7. 如果这表格是来自REPL或函数,那么结果会打印在 Echo Area 中。如果来自键绑定,则可能会影响 Emacs。

所以总的来说,评估通过查找表格中的每个元素的定义并递归地对其求值来工作。它会一直重复此过程,直到得到一个结果值。

这意味着表格可以包含其他表格,并且一切都可以互相嵌套 - 这使 Lisp 成为一门非常表达的语言,可以表示复杂的概念。

对不同类型的对象,评估的工作方式有所不同:

  • 列表:每个元素都会递归地评估
  • 符号:会查找其绑定的值
  • 所有其他类型的对象:通常是自我评估的,意味着它们返回自己的值

某些对象是自我评估的,意味着它们返回其自己的值:

 1: ;; Primitives are usually self-evaluating
 2: 42      ; 自我评估的 - 返回42
 3: "hello" ; 自我评估的 - 返回"hello"
 4: [1 2 (+ 1 2)]
 5: 
 6: '(1 2 3) ; 非自我评估的 - 评估每个元素
 7: 
 8: ;; Not self-evaluating!
 9: buffer-file-name
10: 
11: ;; Evaluates a function!
12: (+ 300 11)
13: 
14: (300 100)
15: 
16: ;; Some representations can't be evaluated!
17: #<buffer Emacs-Lisp-01.org>
18: 
19: 

所以,更具体地:

  • 列表:递归评估每个元素,最终返回最后一个元素的值。
  • 符号:查找其值并返回它。如果未绑定,则引发错误。
  • 数值/字符串/向量/布尔值:自我评估的,返回自身。
  • 函数:调用该函数并返回其返回值。

而对于那些没有代码表示的 Emacs 类型(如缓冲区、窗口等),如果它们出现在一个表格中,通常会引发错误,因为我们无法对它们求值。

所以总的来说,对于任何对象,Emacs 会尝试通过以下方式之一来对其求值:

  1. 如果它是自我评估的,则直接返回其值
  2. 如果它是列表,则递归地对每个元素求值
  3. 如果它是符号,则查找其值
  4. 如果它是函数,则调用它
  5. 否则,引发错误

我希望这有助于澄清 Emacs Lisp 中各种类型的评估方式之间的差异。一旦您理解了求值器如何处理每个类型,许多 Lisp 的奥秘就会显现。

环境

在 Emacs Lisp 中,一切都是相对于全局环境进行评估的!

优点:您可以在运行时更改环境中的任何内容 缺点:您的 Emacs 会话中,环境可能随着时间的推移变得“脏乱”

;; 设置初始值
(setq efs/our-nice-variable "Hello System Crafters!")

;; 将其更改为其他内容(甚至是不同的类型!)
(setq efs/our-nice-variable 1337)

全局环境包含:

  • 所有已定义的函数和变量
  • 所有的键绑定和其他设置
  • 所有加载的包提供的绑定
  • 等等

所以当您对某个表格求值时,Emacs 会在这个环境中查找其元素的定义和值。

这意味着您可以在REPL中定义一些内容,然后立即在另一个缓冲区中使用它 - 因为两者共享同一个环境。

但是,这也意味着您在会话的不同阶段定义的内容会相互影响。如果您定义一个变量,然后在几个小时后忘记它,并再次使用相同的名称,这可能会导致问题。

为解决此问题,Emacs 提供了几种“隔离”环境的方法:

  • 使用 `let` 创建新绑定,它们仅在 `let` 表格内可见。
  • 使用策略像“命名空间”来阻止相互影响的包之间的名称冲突。
  • 重启 Emacs 以清除环境并从头开始。
  • 在单独的 Emacs 实例中评估代码。

所以,总体而言,全局环境的概念为 Emacs Lisp 带来了强大的动态性,但也增加了管理环境变化的复杂性。您需要理解作用域规则,并采取措施来避免不同部分之间的相互影响。

Expressions

Lisp 是一门基于表达式的语言,几乎所有的表格都返回一个值。

;; 一个非常有用的函数...
(defun add-42 (num)
  (+ num 42))

;; 它返回结果
(add-42 58)

;; 在另一个调用中使用结果
(* (add-42 58) 100)

这个简单的示例演示了几个重要的方面:

  1. (defun add-42 ...) 定义一个函数,名称为 add-42
  2. 调用 add-42 时,Lisp 会对其参数 num 求值(此处为 58),将其传递给函数。
  3. add-42 函数对其参数求值,得到 `58`。它然后将 `42` 相加,得到 100
  4. add-42 函数的调用表达式 (add-42 58) 返回函数调用的结果 100
  5. 我们可以使用这个结果作为另一个函数 * 的参数。它将 100100 相乘,得到最终结果 10000

所以这展示了 Lisp 中表达式和求值之间的关系:

  • 几乎每个表达式都返回一个值。
  • 这个值可以用作其他表达式的一部分。
  • 通过这种方式,我们可以构建复杂的表达式来表示各种概念。

这使 Lisp 变成一门非常表达的语言。我们可以表达复杂的算法和逻辑,而它们仍然可以像简单的算术表达式一样易于理解。

Symbols

符号也是一种对象类型,但它不是自我评估的!

符号可以包含字母数字字符以及许多其他字符:

# 可能的符号字符
- + = * / _ ~ ! @ $ % ^ & : < > { } ?

这使您能够根据符号中包含的字符来为符号赋予含义。 一些例子:

  • bui-keyword->symbol - 从一种类型转换为另一种类型
  • efs/some-name - 为符号定义一个“命名空间”
  • *pcache-repositories* - 表示全局变量(在 Emacs Lisp 中不常见)
  • string= - 检查某物是否等于某物
  • 函数名不可以这样求值:

当对符号求值时,它返回与该绑定相关联的变量值:

;; 我们之前看到的示例
buffer-file-name

但是函数名不能像这样求值:

get-file-buffer

我们将在未来的一集中进一步讨论此点。

https://www.gnu.org/software/emacs/manual/html_node/elisp/Symbol-Type.html#Symbol-Type

所以,总结一下:

  • 符号是 Emacs Lisp 中的对象类型,但不是自我评估的。
  • 它们可以包含各种字符以表示其含义。
  • 当对符号求值时,Emacs 会查找其变量绑定并返回其值。如果未找到绑定,则引发错误。
  • 函数和宏的名称也是符号,但不能直接求值。我们必须使用 (函数名 参数) 的形式调用它们。

符号是 Emacs Lisp 中一个非常重要的概念。它们用来:

  • 命名变量、函数、宏等
  • 表示关键字和其他标识符
  • 引用和查找各种绑定
  • 等等

所以理解符号以及如何在 Emacs Lisp 中使用和评估它们是掌握语言的关键。

中序 VS 前序

Lisp 表达式使用“prefix”前缀表示法:

(+ 300 (- 12 1))

这为什么有用呢?因为它使所有函数和运算符具有相同的重要性,甚至包括您定义的函数!

大多数语言使用中序表示法,如:

1: 300 + (12 - 1)

中序表示法需要定义运算符的优先级和结合性,以确定表达式的求值顺序。这使其对阅读和解析变得复杂。

相比之下,前序表示法将运算符视为普通的函数调用。每个子表达式都作为一个参数提供给其相关运算符:

(+ 300 (- 12 1))

这意味着:

  1. 求值始终从左到右进行
  2. 您可以为任何运算符(除了构造函数)定义函数
  3. 用户定义的函数与内置函数具有相同的语义

所以前序表示法简化了 Emacs Lisp 语言的语法和求值规则。我们不需要考虑运算符优先级或结合性 - 我们只需要从左到右顺序地求值每个子表达式。

此外,前序表示法还意味着我们可以轻易地为任何运算符定义函数,包括:

  • +
  • *
  • /
  • and
  • or
  • 等等

这使 Emacs Lisp 变成一门非常可扩展的语言。我们可以轻松地自定义语言的各个方面。

总而言之,前序表示法是 Lisp 家族语言的一项关键特征,为其带来许多实用性和表达能力。一旦您习惯了它,您将开始欣赏其简洁性和一致性。

练习

打开 *scratch* 缓冲区,尝试编写简单的表达式。在每个表达式的末尾使用 C-x C-e (eval-last-sexp)来对其求值。

这里有一些您可以尝试的:

42

(* 42 10)

(concat "Hello " "Emacs!")

;; 简单列表
'(1 2 3)

;; 创建列表的另一种方式
(list 1 2 3)

;; 获取列表中第二个元素
(car (cdr '(1 2 3)))

;; 一个向量
[1 2 3]

此外,还要去看看您的 Emacs 配置,现在可以识别其中的哪些内容!

这是一个很好的练习来加深您对 Emacs Lisp 的理解。我鼓励您尝试:

  • 评估不同类型的表格(数字、字符串、列表、向量等)
  • 使用不同的内置函数(如 concatlistcar 等)
  • 定义一些简单的函数和变量
  • 尝试使用键绑定、菜单和其他命令
  • 检查初始化文件以了解更多配置选项

一些其他您可以尝试的内容:

 1: (defun square (x) (* x x))  ; 定义一个 square 函数
 2: (square 3)                  ; 调用它
 3: 
 4: (setq name "John")         ; 绑定一个变量
 5: name                         ; 查看其值
 6: 
 7: (if (> 5 4)
 8:     "Yes"
 9:   "No")                    ; 使用 if 测试
10: 
11: (when (> 5 4)
12:   (message "5 is greater than 4!")) ; 使用 when
13: 
14: (progn
15:   (message "Hello")
16:   (message "World!"))     ; 使用 progn 执行多条语句
17: 
18: [1 (2 3) "four" ]        ; 一个向量
19: 
20: (length [1 2 3])         ; 获取向量的长度
21: 
22: (aref [1 2 3] 1)         ; 索引访问向量元素

我希望这个练习能帮助您掌握表达式、函数、变量和其他 Emacs Lisp 概念。不要犹豫,在练习中随意提问 - 我很乐意提供更多解释和示例。

订阅系统工匠通讯!
与最新的系统工匠新闻和更新保持同步! 阅读 Newsletter 浏览更多信息。
名称 (optional)
邮箱