从操作系统架构的角度谈Emacs的学习

liang @ 2019年09月16日

如无必要,勿增实体。 ----奥卡姆剃刀

我的一些经历

2014年初,我曾在某东的基础架构组短暂工作过一段时间,工作内容并不多,但当时的领导用Emacs作为编辑器给我留下深刻印象,那是我第一次见到Emacs。

当时他正在编写一个Nginx的插件,开发环境是c,他给我展示了在Mac下高效(花式)使用Emacs的方式,有语法高亮,代码提示,代码跳转以及眼花缭乱的界面切换等等,我能感受到Emacs带给他极大的满足感和自豪感。

他告诉我,在Unix/Linux终端下,Emacs的快捷键是可以使用的,比如在命令行下Ctrl + a就可以回到行首,Ctrl + e就可以回到行尾,Ctrl + Alt + 方向键可以以单词为单位快速移动光标。这三个操作被我牢牢记下,并在日常的工作中广泛使用。也是这三个操作让我对Emacs产生了巨大的兴趣。

我首先在自己的电脑上安装了黑苹果(后来在新公司的电脑上也安装了黑苹果),然后安装上了Emacs,复杂的配置,丑陋的界面,大量的组合键,在很长一段时间里,Emacs给我带来巨大的迷惘和挫败感,卸载了好多次。但每次想到领导使用Emacs时的场景,我想一定是自己打开方式不对,于是又一次次捡起啦。

一晃几年过去,这期间我参看别的配置,对自己的Emacs环境做了很多修改,总是不满意,Emacs依旧很简陋。因为是依葫芦画瓢,觉得有层窗户纸总也捅不破,Lisp也看的莫名其妙。

有时候很郁闷,自己有大几十万行代码量积累,高并发服务,大数据处理,机器学习算法都搞得还不错,怎么连个编辑器都搞不定。难道这个Emacs骨子里就不行,可是为什么有那么多人趋之若鹜?为什么之前的领导能在Emacs上得到这么大的满足感?

后来我多次尝试Spacemacs的配置,但始终对Spacemacs的哲学摸不透,除了界面漂亮一点,我并没有发现Spacemacs的创举在何处?几次三番之后,偶然间搞清楚了Spacemacs的Leader Key的使用,顿时感觉发现了新大陆,从此Emacs操作也开始有点飞起的感觉。后来阅读了Spacemacs的文档,研究了下layer的创建和管理方式,觉得Spacemacs正是自己寻找很久的心仪配置,从架构设计使用方式都是。

有了对Spacemacs的认识后,我又开始认真学习《GNU Emacs Lisp编程入门》《Emacs Lisp 简明教程》这两份教程。学完之后,大有如梦方醒的感觉,原来Lisp如此之简单,Emacs如此之直白,我也长舒一口气,Emacs是名副其实的神的编辑器。

总的来说,之前对Emacs的所有误解,来源于自己对Emacs的认知偏差,以前自己从修改配置的角度只能看到一些边边角角,一旦从操作系统和宏观架构的角度去认识Emacs,就会对Emacs发出由衷的惊叹,Emacs的学习也一下变得简单了。

接下来我想谈谈Emacs的历史,Lisp的设计思想,Emacs的设计架构,Emacs的操作系统属性,Emacs模式等话题,希望从横纵多个角度讨论Emacs。对Emacs有了这些认识之后,相信Emacs对于任何人都会变得简单直白,Emacs能真正提高大家的工作效率和生活质量。

Emacs的历史

我整理了一份年鉴,提供了计算机领域一些重要产品的发布时间,从时间上感受下Emacs的历史。

  1. Unix在1970年诞生于Bell实验室,最早运行于PDP-7主机上。
  2. Emacs在1970年代早期诞生于MIT人工智能实验室(MIT AI Lab),最早运行于PDP-10系统。
  3. GNU是一个自由的操作系统,其内容软件完全以GPL方式发布。1983年9月27日,理查·斯托曼在 net.unix-wizards 和 net.usoft新闻组中公布这项计划。
  4. Linux是一种自由和开放源码的类UNIX操作系统。该操作系统的内核由Linus·Benedict·Torvalds在1991年10月5日首次发布
  5. Classic Mac OS,系统搭载在1984年销售的首部Mac与其后代上,采用Mach作为内核,在Mac OS 7.6.1以前用“System vX.X”来称呼。末代版本是Mac OS 9。
  6. Microsoft Windows是微软公司推出的一系列操作系统。其问世时间为1985年,起初为运行于MS-DOS之下的桌面环境。
  7. macOS(2012年前称Mac OS X,2012年-2016年称OS X),结合BSD Unix、OpenStep和Mac OS 9的元素。它的最底层建基于Unix基础,其核心代码被称为Darwin,实行的是部分开放源代码。

可见,在Emacs的早期版本发布时,Linux,Windows,Mac OS这些我们习以为常的操作系统都还没有被开发出来,Emacs的设计思想是非常超前的。那Emacs是不是老古董了呢?是不是倚老卖老了呢?我觉得不是。

Lisp设计思想

Lisp是以列表形式,实现函数式功能的解释性语言。

Lisp(历史上拼写为LISP)是具有悠久历史的计算机编程语言家族,有独特和完全括号的前缀符号表示法。起源于公元1958年,是现今第二悠久而仍广泛使用的高端编程语言。只有FORTRAN编程语言比它更早一年。Lisp编程语族已经演变出许多种方言。现代最著名的通用编程语种是Clojure、Common Lisp和Scheme。

Lisp被设计出来时被用于人工智能处理(早期的基于符号处理的人工智能领域,以图灵测试为目标),关于人工智能发展的几个时期有时间再细聊。

简单讲,以三个个简单特性构成了整个Lisp:

  1. 函数式编程
  2. 列表处理
  3. 解释求值

Lisp是指"LISt Processing"(列表处理),和通过把列表放置在括号之间来处理列表(甚至是列表的列表)的编程语言。括号标记了列表的边界。列表是Lisp的基础。

Emacs Basic

上图定义了1 + 2 + 3 + 4 + 5 = 15,括号及其中包含的元素构成一个列表,列表的首元素是一个函数,剩余的元素是这个函数的参数,参数可以0个或者多个,参数还可以是函数(比如:(+ 1 2 (+ 3 4 5))的元素包含了一个列表),因此Lisp的列表定义是递归的。在Emacs的任何Buffer里,通过C-x C-e可以随时对一个列表求值。

到这里,基本已经搞定Lisp 90%的内容了,我们只要把列表首元素换成其他函数就能完成不同的功能,这就是Lisp,就是这么简单。那究竟有哪些函数可以用呢?C-h f会列出所有已经定义的函数,选择一个可以查看函数的描述。此外,我们还可以用defun关键字自定义函数,这个可以自己学一下,这是Lisp剩下的10%。

Lisp可能是大家能学习到的最简练的编程语言了,连他的定义都是递归的,这在现代编程语言中简直不敢想象。

ps: 作为惯例,Lisp版的Hello World

(message "Hello, World!")

作为函数式编程语言,函数在Lisp里是第一等公民,可以作为值和参数使用。

Emacs操作系统属性

Emacs是单进程单线程,内存空间不分区。

就像大多数编程语言一样,Lisp提供了一系列关键字来定义变量(setq, let),常量(defconstant),函数(defun)等,大家平时修改Emacs配置时对这部分最熟悉了,但Emacs在这方面做得更加极致,很多初学者不能从操作系统的宏观角度来看待,因此总觉得与Emacs隔着一层薄薄的窗纱,对Emacs的修改也做不到随心所欲。

我们知道,OS上的任何一个进程都有自己的内存空间,内存用来保存运行时的变量,常量,对象等,而不同的编程语言支持的运行环境通常会将内存划分为不同的区,最典型的是Java虚拟机将内存划分为线程共享内存区(方法区),以及线程私有内存区(虚拟机栈本地方法栈程序计数器),共5个区,作为单进程多线程的Java程序,通过将内存分区把进程共享变量和线程私有变量分开存放,各自管理,同时提高了垃圾回收的精细控制。

Java内存区

Emacs作为Lisp语言的运行环境,本身就是个虚拟机,虚拟机的核心工作是管理内存。Emacs作为OS上的一个单进程单线程实例,内存空间非常简单 ---- 不分区。Emacs定义的内建变量和函数,与用户定义的函数统统放在一起,而在Lisp里函数与变量是等价的。由此我们可以得到以下结论:

  • Emacs是一个单进程单线程实例。
  • Emacs是一个Lisp运行环境,是个虚拟机。
  • Emacs作为虚拟机,只有一个宏观内存区,所有变量和函数均放置一起。
  • Emacs有局部变量(let定义),局部变量在函数中有效。
  • Emacs有一批内建(build-in)的变量和函数,用户可以使用。
  • Emacs的内建(build-in)变量和函数可以被用户修改,进而修改Emacs的上下文,实现定制Emacs(note: defvar定义变量,不会修改已经存在的变量值)。
  • Emacs支持用户新建变量和函数,从而为Emacs增加功能。
  • Emacs支持用户在任何地方通过C-x C-e执行Lisp代码,代码运行中定义的变量和函数会被保存到Emacs内存中,等同于为Emacs增加功能。

我们可以看到,不同于Java虚拟机等运行环境将系统内建的类,变量,函数单独管理(通过系统类加载器委托加载),Emacs将内建(build-in)变量和函数开放给用户,任何人都可以修改,这也是Emacs高可扩展的由来。不同于其他程序往往在启动时加载配置文件,我们可以任何时候通过C-x C-e执行Lisp代码,添加或者修改Emacs运行环境的变量和函数,从而实现对Emacs的实时配置。

Emacs Memory Space

Emacs中的函数可以分为交互式函数和普通函数,交互式函数是可以通过M-x执行,普通函数可以通过C-x C-e求值。要查看当前运行环境中定义(内建和用户定义)的变量和函数,可以通过下面的方法:

  • C-h f: 查看函数列表和描述。
  • C-h v: 查看变量列表和描述。

Emacs help

例子

我们定义一个函数,让当前buffer的输入区居中。随便开一个buffer(哪怕是当前buffer也行),通过如下代码定义一个非交互的普通函数buffer-center,然后执行函数。我们发现,切换到其他buffer,再切换回来时,当前输入区居中显示了。

;; 请使用C-x C-e分别对下面两个表达式求值

(defun buffer-center ()
  "Set margins in current buffer."
  (setq left-margin-width 24)
  (setq right-margin-width 24))
  
(buffer-center)

Emacs Center Margin

扩展

你有没有想过org-mode, markdown-mode, java-mode, python-mode, javascript-mode, eww-mode, company-mode是怎么开发出来的?
你有没有想过给自己开发一个major-mode?
你有没有想过如何实现语法高亮?
你有没有想过如何实现语法补全?

通常,对Emacs的扩展往往集中在几个地方:主题,字体,键绑定,语言支持,交互式函数,Emacs提供了很多内建变量和函数定义,我们可以通过继承来修改已有模式的定义,也可以添加新模式和新功能。

比如:在Emacs要增加新增的语言支持(如:关键词高亮,括号匹配,缩进)是通过正则表达式+Emacs Face实现的,有时间我们可以单独讲,感兴趣的可以参考:

Emacs模式设计

我们知道,Emacs的编辑对象是Buffer,每个Buffer可以有一个Major Mode和多个Minor Mode,这又是Emacs的一大创举。

Emacs对于Buffer的处理非常简单,一旦一个Buffer被标识为某个模式,Emacs就会为这个Buffer提供该模式定义的服务,比如:语法高亮,智能补全,括号匹配,代码格式化。往往一个模式不会定义所有的功能,因此我们可以给Buffer设定多个Minor Mode,从而让一个Buffer享受多个模式提供的服务。

通常,Emacs根据文件后缀为Buffer设定一个Major Mode,比如:*.csv对应CSV Mode*.sql对应SQL Mode*.org对应Org Mode*.md对应Markdown Mode

通常,Emacs会根据Major Mode自动为Buffer设定几个Minor Mode,我们也可以通过M-x为Buffer开启或者关闭模式。比如,当我们在*scretch*(即草稿Buffer)里写SQL时,可以通过M-x sql-mode为无模式的*scretch*开启SQL Mode

在一个Buffer里提供多种模式的设计,很少出现在其他编辑器里,这个必须要赞一下。

Emacs Buffer Mode

如上图,我们在Emacs里打开一个.md文件,默认该Buffer的主模式(Major Mode)会被设置为图中红色框的Markdown模式,然后在绿色框里关联提供了如下几个次模式(Minor Mode):

  • OrgTbl: 用来格式化表格。
  • a: company-mode,用来自动补全。
  • y: yas-minor-mode,用来提供模板替换功能。
  • p: smartparens-mode,用来管理成对的标点符号例如括号,方括号,花括号,引号,尖括号, 还有那些让你痛苦的成对的符号。
  • h: hybrid-mode,Spacemacs提供的Vim和Emacs快捷键混合输入模式,即在Evil的Normal模式下使用Vim快捷键,在Insert模式下使用Emacs快捷键。
  • K: which-key-mode,它在当前输入的不完整命令( 前缀) 中显示键绑定,用来做快捷键提示。
  • L: visual-line-mode,提供自动换行功能。

另外,我还手动添加了一个M-x window-margin-mode,让当前Buffer只能输入80列,这样写文档看起来排版比较紧凑,所以大家看截图右边有一片是空白,就是设置了margin的原因,具体代码参见emacs window margin

  • Margin: 设置当前Buffer的输入区域为80列。

Emacs哲学思想

Emacs的哲学是简单,Lisp语言如此简单,它的架构设计如此简单,它的线程结构如此简单,它的内存空间布局如此简单,它的使用方式如此简单。它的简单是"奥卡姆剃刀原则"的完美体现。

Emacs是GNU的第一个产品,与之同期的Unix,以及之后的GNU/Linux也遵循了这一设计思想,这是因为在那个时代简单是软件设计的必然选择。毕竟,在那个时代还没有复杂的面向对象,没有悲剧的《人月神话》,没有冗杂的软件工程。那时候的软件是纯粹的。

我觉得每个人都应该尝试下Emacs,感受它背后的简单纯粹。那些喊着"人生苦短,我用xxx"的朋友,是时候放下酒杯,吃点菜了。

写于王府世纪

引用

王垠 Chez Scheme 的传说
王垠 Lisp 已死,Lisp 万岁!
王垠 一种新的操作系统设计
Wikipedia Microsoft Windows
Wikipedia Mac OS操作系统
Wikipedia Linux
Wikipedia UNIX
Wikipedia Lisp
Wikipedia GNU