简介

为什么学习 Emacs Lisp

Emacs Lisp 是在 Emacs 中使用的编程语言, 用来扩展 Emacs 编辑器, 所以这
个语言基本上离开 Emacs 之后, 就没有用武之地了. 但是因为 Emacs 的强大和
广泛使用, 使得 Emacs Lisp 的使用者非常多. 基本上每一个使用 Emacs 工
作的人, 在用了一段时间的 Emacs 之后, 都会像学习一下 Emacs Lisp 以便打
造自己个性化的 Emacs. Emacs 的成果在于它的灵活性, 而这种灵活性就是
Emacs lisp 赋予它的. 几乎任何的文本相关的工作, 都可以在 Emacs 中完成,
所以使用 Emacs 的人很容易, 就陷入 Emacs 中而不能自拔. 如果遇到了一些工
作虽然可以在 Emacs 中完成, 但是不是 Emacs 用起来不是那么的得心应手, 那
就说明你没有找到正确的使用 Emacs 的方法, 也就是为 Emacs 挂着正确的插件,
如果没有这样的插件存在, 那么就需要你自己动手写一个了.

还有些时候, 虽然完成自己工作的 Emacs 插件已经有了, 但是这个插件的效果
和自己的预期不是完全的一致, 这个时候, 如果插件足够的灵活, 也许配置一下
就可以了, 如果不是那么的灵活, 也许就需要自己动手 Hacker 一下. 有了
Emacs Lisp 的知识, 就能完成这样的工作了.

大部分的 Emacs Lisp 的工作就是在Emacs 中运行, 与 Emacs 交互. 但是
Emacs Lisp 是一个图零完备的语言, 也就是说, 理论上 Emacs lisp 有能力完
成其他编程语言完成的任何工作. 如果只是从学习编程语言的角度来说的话,
Elisp 其实也是值得学习的. 尤其是你常常用 Emacs 工作的话.

Lisp 的历史

Emacs Lisp 这个语言, 是 Lisp 的一个方言. 说到 Lisp, 话题就多了. 首先,
这个语言有悠久的历史. Lisp 是还在使用的第二古老的高级语言, 只比最古老
的高级编程语言 Fortran 晚了一年. 其次, Lisp 方言总多. Lisp 方言多的原
因是因为 Lisp 作为一种编程语言, 语法非常的简单, 为这样一个语言写一个解
释器相等的容易. 解释器只需要实现很少的基本功能, 剩下的内容就可以使用
Lisp 自己来编写. Lisp 方言总多有利有弊, 利是让这门语言称为了一个方便的
语言特性试验场. 编程语言的大部分理论, 都可以在 Lisp 中很快的实现. 弊端
是, Lisp 各个分支的不统一, 使得 Lisp 的用户难以写出通用的程序. 也是因
为这个原因, 现在基本上Lisp 方言统一到了两大阵营中: Common Lisp 和
Scheme. 这两大阵营是有自己的标准的, Common Lisp 是美国国家标准委员会指
定的标准, 而 Scheme 的标准有电气电子工程学会 (IEEE) 制定. 所以要分成两
个整营是因为双方对语言的要求不一样. Common Lisp 主要是为了让 Lisp 在实
际的编程中发挥作用, 且让写出的程序能够通用而指定的标准. 而 Scheme 实践
的是最小化哲学, 这样 Scheme 本身就可以非常的简单, 但同时也就牺牲了这个
语言在实际编程中的普及. 因为这个语言的库实在是太小了.

Emacs Lisp 没有投靠这两大整营的任何一个, 因为它背靠着 Emacs 这座大山.
但是从血统上来说的话 Emacs Lisp 和 Common Lisp 的关系更亲. 但是无论怎
么说 Common Lisp 与 Emacs Lisp 都是两个不同的方言. Common Lisp 是把
Lisp 作为通用目的的语言来设计的, 而 Emacs Lisp 是作为 Emacs 编辑器的脚
本语言来用的.

Emacs Lisp 是 Emacs编辑器的脚本语言, 这大概会让你联想到 Javascript 最
初是作为浏览器的脚本语言设计的. Emacs Lisp 在 Emacs 中扮演的角色的确和
Javascript 在浏览器中扮演的角色相似. 但实际上, Emacs Lisp 对于 Emacs
的作用要比 Javascript 在浏览器中的作用大的多. Javascript 是浏览器发布
之后才添加到浏览器中的, 而且只是浏览器中的一个重要的部件之一. 到现在依
旧可以在浏览器中把 Javascript 的功能彻底的关闭的. 但是 Emacs 是无法把
Emacs lisp 关闭的. 因为 Emacs 中的大部分的编辑功能都是 Emacs Lisp 实现
的, 很少的部分是用 C 写的(其中还包括了 Lisp 解释器). 所以 Emacs 中完全
不运行 Emacs lisp, 那么很可能最基本的编辑功能都无法使用.

Lisp 的特点

但是无论如何 Emacs Lisp 毕竟是 Lisp 方言. 那么作为 Lisp 语言, 那么
Lisp 语言的特征是什么呢? Lisp 语言的特征是体现在这个语言的语法上的, 这
个语言以括号和前缀标记为特征.

括号不难理解, 什么叫前缀标记呢? 这实际上说的是操作符的位置. 比如说在数
学上, 表示两个数相加是 $a+b$ 这里 + 是操作符, 但是它是放在两个数中间
的, 而表示一个函数的值的时候, 使用$f(a,b)$, 实际上这里符号 f 的作用
+ 是一样的, 因为 f 表示的也是作用到它的参数上的一套规则, 所以
f 也是操作符, 但是这个操作符是放在参数的前面的. 一般的编程语言都是和
数学类似, 操作符的位置既有在操作数中间的也有在最前面的. 这不难理解, 因
为数学的发展要比计算机早, 计算机语言大部分这样设计, 就是为了照顾人类已
经熟悉的习惯. 但是这样做其实是有一个问题的,那就是结合律和优先级的问题,
比如公式 $a+b\times c+d$ 很可能像表达的实际意思是 $(a+b)\times (c+d)$,
但实际上安装结合律和优先级设定公式的表达的意思却是
$a+(b\times c)+d$. 但是用括号前缀标记就不会有这样的问题.

完全使用前缀操作还有个好处是, 一个操作符可以很容易的定义多个操作数. 比
如对于
$$
1+2+3+5+6+7+8+9+10 \label{sum1},
$$
同样的意义, 数学上用前缀表示的方案就是
$$
\sum_{1}^5i \label{sum2},
$$
很显然 $\ref{sum2}$ 要比 $\ref{sum1}$ 表示简洁的多.

Lisp 语言以前缀和括号作为特征, 通过用是 Lisp 表示

\begin{align}
(1+2)\times{}(3+4) \
1+(2\times{}3)+4
\end{align}

可以就能很好的体现 Lisp 的括号与前缀标记特性了.

(* (+ 1 2) (+ 3 4)) ;(1+2)*(3 +4)
(+ 1 (* 2 3) 4)     ;(1+(2*3)+4)

上面的代码, 只要我们知道 * 表示的是对其参数做乘法运算, + 是对其参
数做加法运算, 就能完全无无解的知道答案, 且不会出现歧义.

那么你是怎么做的呢? 或者说 Lisp 解释器是怎么解释上面的代码呢? 粗略来讲
是这样的, 小括号括起来的这样的一个语法, 在 Lisp 叫做一个表单
(from). 一个表单实际上就是一个列表结构. Lisp 解释器拿到一个表单之后
一般 执行如下步骤:

  1. 获取第一个元素的值, 并查看这个其是否为一个函数, 如果不是一个函数,
    那么就报错, 如果是一个函数, 执行第二步
  2. 把这个表单后面的所以内容都作为传给这个函数的参数, 并从左到右依次计算
    参数的值
  3. 调用以参数调用这个函数

上面所以说 一般 执行的步骤, 那么有特殊情况是什么呢? 特殊情况出现在对
特殊表达的处理上, 特殊表单不一定计算它所有的参数. 比如逻辑操作, and
当有一个参数为 false 时, 无论后面的参数是什么, 结果一定是 false,
所以一般的其他语言在逻辑操作上也进行所谓的短路操作, 在 Lisp 中使用特殊
表单表示这类操作. 因为不像其他表单那样要计算所以的参数, 看起来比较特殊,
所以叫特殊表单.

需要提醒大家的是, 现在我们说的特性都是 Lisp 的特征, 而不是 Emacs lisp
的特征, 这是所有 Lisp 语言都有的特征. 到目前为止, 对于 Emacs Lisp 独特
的东西, 我们还一点没讲. 不过不用担心, Lisp 的知识也是适用与 Emacs
Lisp. Emacs Lisp 常常也用 Elisp 缩写表示. 后面的章节如果我说这是 Lisp
的语法, 那就是说这个语法适用于所有的 lisp 语言, 如果没有特意的强调是
Lisp 的语法, 那默认就是 Elisp 特有的语法.

Elisp 的语言类型

上面说过了 Elisp 是作为 Emacs 的脚本语言而设计的, 所以我们能说 Elisp
是一个解释性的语言吗? 说 Elisp 是一个解释性的语言当然没错. 因为如果你
写好源代码, 的确可以用 Emacs 中的 Elisp 解析器来解释允许代码的. 但是
Elisp 有和 Bash 这样的纯粹的解释语言不同. Elisp 的代码实际上可以编译成
为字节码, 然后由字节码解释器 (也就是一个虚拟机了) 来允许. 所以你也可以
说 Elisp 也是一个编译语言.

虽然 Elisp 在编译和解释这个范畴中跨界, 但是动态类型还是静态类型上
Elisp 却是坚定立场的. Elisp 是动态类型语言, 也对 Elisp 来说, 变量其实
是没有类型的, 只有对象有类型. 一个变量可以指向不同的对象 (这用 Lisp 中
术语叫做重新绑定), 当然了不同的对象的类型是可以不一样的. Elisp 是了强
类型的语言, 也就是说函数的参数类型必须是函数需要的类型, 否则就会可能出
错. 所以在语言类型这个范畴上, Python 其实与 Lisp 最像. 可能这也就是
Python 从 Lisp 受到的影响吧.

Elisp 中的 "对象"

在上面的描述中, 我用到了一个术语 "对象", 但是对象是指的是什么呢? 对象是英
文 Object 的翻译, 中文里和这个词更接近的应该是 "物体", 或 "东西", 翻译成对象
之后反而让人迷惑. 但是因为大家都习惯了这种不符合汉语习惯的表达, 所以现
在如果再有人在计算机领域的书籍中把 Object 翻译成 "物体" 或 "东西" 我想,
读者可能也会误解. 其实在台湾, Object 就是翻译成 "物件" 的. 我觉得 "物
价" 要比 "对象" 更好一点. 说了这一大通跑题的话, 我想说什么呢? 在面向对
象的语言中, 表示一个物体, 既要表示他的数据特征, 还要表示它的行为特征,
这两个特征结合在一起构成了一个对象. 而在 Elisp 中说的对象,
就是一个物体, 没有什么特别的含义, 就像日常的语言中说一个物体一样的意义.

所以 Elisp 中一个对象, 表示的是在 Elisp 解释器(或虚拟机) 中用唯一一块
内存表示的东西. 这是我们讲的 Elisp 的第一个概念.

Elisp 作为一个编程语言, 它提供了多少种内建的对象呢? 所谓内建对象, 就是
这些对象是由语言本身提供的, 而不是程序员自己定义的. 因为 Elisp 解释器
(虚拟机) 是用 C 语言实现的, 所以 Elisp 内建对象就是用 C 实现的对象. 这
些内建对象有如下几类:

  • 符号: Elisp 中的符号是一个数据结构, 这个数据结构有这样几个字段:
    • 不可变的 name 字段, 保存一个字符串, 表示这个符号的名字
    • value 字段, 可以指向任意的对象, 用来做变量使用
    • function 字段, 用来作为函数, 宏名的时候使用
    • plist 字段, 用来记录这个符号的属性
  • 整数: Elisp 中只有有符号整数, 没有无符号整数. 这和现代的编程语言比
    较像, 比如 Java.
  • 浮点数: 浮点数是用 C 语言中的 double 类型实现的
  • cons类型: cons 类型是一个数据结构, 用 C 语言的术语来说就是: 这个结
    构有两个域其中一个叫 CAR 另一个叫 CDR 但是这两个域的
    可以存放的类型没有限制. 或者可以说域 CARCDR 可以
    指向任意的对象.
  • 字符串: 用来表示文本的字符序列.
  • 向量: 这也是一个数据结构, 这个结构分配了固定长度的, 连续的分配的空
    间, 每个空间都可以指向任意的对象.
  • 内建函数 (subr) : C 语言实现的函数.

除了这集中对象外, 其他的对象都是用这些对象构建的.

除了这这种分类方法外, 还可以把 Elisp 的对象分成编程对象和编辑对象. 编
程对象是说在把 Elisp 作为一个编程语言对待是用到的对象. 而编辑对象是要
完成文本编辑任务是, Elisp 与 Emacs 交互用到的对象.

根据这个分类, 本书分成了两个部分, 第一个部分讲 Elisp 的语言核心, 基本
不涉及或很少设计到和 Emacs 的交互. 第二部分讲 Elisp 如何与 Emacs 交互
来完成各种任务.

Elisp 编程环境的设置

在这一章的最后, 讲一下如何设置 Elisp 的编程环境. Elisp 的编程环境的设
置极其简单只要你安装好 Emacs 就行. 安装好 Emacs 那么 Elisp 的所有环境
就都安装好了. Emacs 本身就是 Elisp 的集成开发环境. 所以 "设置 Elisp 的
编程环境" 实际上是如何在 Emacs 中更高效的编辑, 运行 和调试 Elisp 代码而
已. 学习编程语言, 最高效的方法也许就是在 REPL中学习了. 要知
道 REPL 是 Lisp 首先引入到编程语言中的, 所以 Elisp 必然也是提供了 REPL
的. 那么怎么在 Emacs 中开启 Elisp 的 REPL呢? 两种方法:

  • 启用 *irlm* 模式: 使用 M-x irlm 启用 *irlm* 模式, 就启用了一
    个 Elisp 的 REPL.
  • 运行 eshell: eshll 是用 elisp 编写的兼容 shell 的程序, 大部分的
    shell 命令可以在 eshlle 中使用, 同时还可以调用 elisp
    的语言. 通过 M-x eshlle 运行 eshell.

如果想把 Elisp 写到文件中, 然后作为脚本来允许的话, 可以在命令行中使用
这样的命令 emacs --batch --script elispfile.el. 这里 --batch 参数
的作用是告诉 emacs 只是做批处理, 而不做编辑的工作, 脚本运行结束后直接
退出进程. --script 用来指定要运行的脚本文件.

可以不可以一边在 Emacs 中编辑 Elisp 源代码, 一边就允许 Elisp 的代码呢?
比如说一个函数写好之后, 立刻对这个函数做测试. 这也是可以的. 在 Emacs
中编辑文本的时候, 按下 C-x C-e Emacs 立即计算光标前一个对象的 Elisp
值, 计算的结果会显示到 Emacs 最下方的, 同时记录到 *Message* buffer
中. 这样, 就可以一边编写 Elisp 源代码, 一边测试了.

至于怎么调试, 怎么编译 Elisp 代码, 在后面的的章节我们再讲.

在计算机中, 物体归根结底还是要用内存表示, 所以计算集中两个不同的对象,
就是两块不同的内存. 如果是同一块内存, 只是这个用不同的另外两块内存记录
这同一块内存的地址, 那么这两个内存地址 "所指" 的对象还是一个同一个对象.
而在两个不同的内存中, 记录相同的数据, 因为是两块内存, 所以是两个内容一
样的对象, 内容虽然一样, 但依旧是两个独立的对象. 用一个比喻来说, 两个变
量 "指向" 同一个对象, 就像一个人有两个名字, 比如孙行者与行者孙. 两个名
字虽然不一样, 但是这两个名字都是 "指" 同一个东西. 而同卵双胞胎, 虽然
DNA 完全一样, 但是两个人还是分别独立的个人, 一个人的死亡并不影响另一个
人的存活. 而孙行者和行者孙可就不一样了, 如果可以真的把孙行者弄死了, 就
不会有后来的行者孙的啥事了.

你可能感兴趣的:(简介)