¶我们将涵盖什么?
这是一个实用系列!
- 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 中的类型和值是掌握语言的重要一步。我希望这个概述能给您一个很好的起点。请随时提出任何问题!
¶Emacs Types
Emacs Lisp 还有许多专用于 Emacs 的类型,其中大多数类型没有代码表达式:
- 缓冲区:文件/文本的内存表示
- 窗口:Emacs 屏幕上的视图
- 布局:窗口的排列方式
- 键绑定:键与命令的映射
- 面板:特殊的显示区域
- 监视器:用于跟踪变量/缓冲区的工具
- 日志缓冲区:用于消息/日志的特殊缓冲区
- Frames
- Threads
- 等等
使用这些 Emacs 专用类型,我们可以影响 Emacs 界面:
;; Get the previous buffer and switch to it (switch-to-buffer (other-buffer))
例如:
- 我们可以创建、删除、重命名和修改缓冲区来打开/关闭文件并编辑文本。
- 我们可以创建、删除和排列窗口来组织我们的屏幕布局。
- 我们可以定义键绑定来为键盘快捷键分配命令。
- 我们可以使用面板显示定制数据和接口。
- 我们可以使用日志缓冲区在 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
等命令时,就会发生评估。
评估的工作原理如下:
- Emacs 会找出当前的“表格” - 这可能是:
- 您刚刚输入的表达式
- 函数内的表达式
- 让我们假设它是
(+ 1 2)
- Emacs 会调用 Lisp 求值器来对表格求值。它会检查表格的第一个元素, 在这种情况下是符号
+
。 - Emacs查找
+
的定义,并调用它。由于+
是一个内置函数,所以它执行加法运算。 +
函数对其参数1
和2
进行求值,得到两个数值。+
函数将这些数值相加,得出结果3
。- 这个结果
3
成为整个表格(+ 1 2)
的值。它被“返回”给调用方。 - 如果这表格是来自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 会尝试通过以下方式之一来对其求值:
- 如果它是自我评估的,则直接返回其值
- 如果它是列表,则递归地对每个元素求值
- 如果它是符号,则查找其值
- 如果它是函数,则调用它
- 否则,引发错误
我希望这有助于澄清 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)
这个简单的示例演示了几个重要的方面:
(defun add-42 ...)
定义一个函数,名称为add-42
。- 调用
add-42
时,Lisp 会对其参数num
求值(此处为 58),将其传递给函数。 add-42
函数对其参数求值,得到 `58`。它然后将 `42` 相加,得到100
。add-42
函数的调用表达式(add-42 58)
返回函数调用的结果100
。- 我们可以使用这个结果作为另一个函数
*
的参数。它将100
与100
相乘,得到最终结果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))
这意味着:
- 求值始终从左到右进行
- 您可以为任何运算符(除了构造函数)定义函数
- 用户定义的函数与内置函数具有相同的语义
所以前序表示法简化了 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 的理解。我鼓励您尝试:
- 评估不同类型的表格(数字、字符串、列表、向量等)
- 使用不同的内置函数(如
concat
、list
、car
等) - 定义一些简单的函数和变量
- 尝试使用键绑定、菜单和其他命令
- 检查初始化文件以了解更多配置选项
一些其他您可以尝试的内容:
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 概念。不要犹豫,在练习中随意提问 - 我很乐意提供更多解释和示例。