Curator 介绍

本文翻译自 Netflix[1]技术博客文章《Introducing Curator - The Netflix ZooKeeper Library》,原文由 Netflix 工程师,Curator 作者 Jordan Zimmerman 编写,原文在这里。接触 Curator 已经快一年时间,期间有过写一篇介绍性文章的念头,但一直没有动手,后来回顾手头的资料,觉得其实这篇文章虽然简洁了一点,但也是一个很好的介绍,于是翻译在这里。仅用于学习,请勿用于其它用途。


Netflix 的开源

Netflix 致力于开源,我们过去写过相关的博客,今天我们公布 Netflix 开源项目的门户网页。网页托管在 Github 上,有几个项目在并行推进当中(其中包括我们今天对外发布的 Curator):

  • Curator - Netflix 的 ZooKeeper 库
  • Astyanax - Netflix 的 Cassandra 客户端
  • Priam - Cassandra 的跨进程备份/恢复功能,Token 管理,配置中心化管理
  • CassJMeter - 运行 Cassandra 测试的 JMeter 插件

ZooKeeper

ZooKeeper 是一个高性能的分布式协调服务框架。它将通用的服务,如命名,配置管理,同步,分组服务等,通过简单的接口展现出来。要全面的了解 ZooKeeper,请参考以下网页:

难以用好

ZooKeeper 本身自带一个 Java 客户端,但使用这个客户端繁琐[2]而且容易出错。客户端的使用者需要做大量的手动维护性工作。比如:

连接问题

  • 初始化连接:ZooKeeper 客户端与服务器进行握手,这需要花一些时间。如果握手未完成,任何要与服务器端同步执行的方法(如,create(),getData()等)都会抛出异常。
  • Failover:如果 ZooKeeper 客户端与服务器连接断开,它会 failover 到集群中另外一台服务器。然后,这个过程会使客户端退回到”初始化连接”的模式。
  • Session 过期:有些边际情况可以导致 ZooKeeper session 过期。客户端需要监视这个状态,关闭并重建 ZooKeeper 客户端实例。

恢复问题

  • 当在 Server 创建顺序节点(sequential ZNode)时,有可能出现这种情况:节点成功创建了,但 server 在将节点名返回给客户端之前崩溃了。
  • ZooKeeper 客户端可能会抛出几个可恢复的异常,使用者需要捕捉这些异常并做重试操作。

Recipe[3]方面

  • 标准的 ZooKeeper recipe(如锁,选 leader 等)只是得到最低程序的描述,要正确地编写出来比较困难。
  • 有一些重要的边界情况在 recipe 描述里没有提到。例如,锁 recipe 的描述中,没有说到如何处理服务器成功创建了顺序(Sequential)/临时(Ephemeral)节点,但在向客户端返回结点名之前就崩溃的情况。如果没有得到正确处理,可能会导致死锁。
  • 某些使用场景下,必须要注意可能出现的连接问题。例如,选 leader 过程要监视连接的稳定性。如果连接到的服务器崩溃了,leader 就不能假定自己继续为 leader,除非已经成功 failover 到另外的服务器。

上述问题(和其它类似的问题)必须由每个 ZooKeeper 使用者来处理。问题解决方案既不容易编写,也不是显而易见的,需要消耗相当多的时间。而 Curator 处理了所有的问题。

Curator 是什么

Curator n ˈkyoor͝ˌātər:,展品或者其它收藏品的看守者,管理员,ZooKeeper 的 Keeper。它由 3 个相关的项目组成:

  • curator-client - ZooKeeper 自带客户端的替代者,它负责处理低层次的维护工作,并提供某些有用的小功能
  • curator-framework - Curator Framework 大大地简化 ZooKeeper 使用的高层次 API。它在 ZooKeeper 客户端之上添加了很多功能,并处理了与 ZooKeeper 集群连接管理和重试操作的复杂性。
  • curator-recipes - ZooKeeper 某些通用 recipe 的实现。它是基于 Curator Framework 之上实现的。

Curator 专注于锁,选 Leader 等这些 recipe。大部分对 ZooKeeper 感兴趣的人不需要关心连接管理等细节。他们想要的只是简单的使用这些 recipe。Curator 就是以此作为目标。

Curator 通过以下方式处理了使用 ZooKeeper 的复杂度:

  • 重试机制:Curator 支持可插拔式的(pluggable)重试机制。所有会产生可恢复异常的 ZooKeeper 操作都会在配置好的重试策略下得到重试。Curator 自带了几个标准的重试策略(如二元指数后退策略)。
  • 连接状态监视:Curator 不断监视 ZooKeeper 连接的状态,Curator 用户可以监听连接状态变化并相应的作出回应。
  • ZooKeeper 客户端实例管理:Curator 通过标准的 ZooKeeper 类实例来管理与 ZooKeeper 集群的实际连接。然而,这些实例是管理在内部(尽管你若需要也可以访问),在需要的时候被重新创建。因此,Curator 提供了对 ZooKeeper 集群的可靠处理(不像 ZooKeeper 自带的实现)。
  • 正确,可靠的 recipe:Curator 实现了大部分重要的 ZooKeeper recipe(还有一些附加的 recipe)。它们的实现使用了 ZooKeeper 的最佳实践,处理了所有已知的边界情况(像前面所说的)。
  • Curator 专注于那些让你的代码更强健,因为你完全专心于你感兴趣的 ZooKeeper 功能,而不用担心怎么正确完成那些的维护性工作。

ZooKeeper 在 Netflix

ZooKeeper/Curator 在 Netflix 得到了广泛的使用。使用情景有:

  • InterProcessMutex 在各种顺序 ID 生成器中被用来保证值的唯一性
  • Cassandra 备份
  • TrackID 服务
  • 我们的 Chukwa 收集器使用 LeaderSelector 来做各种维护性的任务
  • 我们用了一些第三方的服务,但它们只允许有限数目的并发用户。InterProcessSemphore 被用来处理这个。
  • 各种 Cache

获取 Curator

注:
[1] Netflix 是一个美国的在线 DVD 租赁公司,后来转型到网络流媒体服务。参考维基百科 Netflix 词条
[2] 原文为 non-trivial,意思应该是搞反了。
[3] Recipe 中文意思为“菜谱,配方”,感觉翻译过来不能很好的表达英文”一系列约定步骤”的意思,故不译。
[4] Curator 在去年(2013 年)年中已经正式成为 Apache Incubator 项目,代码改为托管在 Github Apache 的对应目录下。主页和文档的位置也有所变化。

嵌套反引用有害

本文翻译自《Nested Backquotes considered harmful》,只用于学习,请勿用于其它用途。


好吧,我想嵌套反引用(nested backquote)不是真的有害,因为你的确可以用它来做一些没有它就很难做到的事情;然而,它又的确会导致非常难以理解的代码(它对于混乱代码比赛[1]或许是有用的,但非常难以维护)。实际上,这篇文章的标题应该叫做“嵌套反引用令我头痛”,但我一直想写一篇标题叫做“那谁谁有害”的文章,于是,标题就是这个了。;-)

注:如果你不知道什么是嵌套反引用,它为什么有用,或者感兴趣于了解更多关于写出或理解这种典型地由嵌套反引用生成的,用于生成代码的代码,那么 Alan Bawden 的杰出论文《Quasiquotation in Lisp》值得一看(这是 Jens Axel Søgaard 所推荐的)。Alan 的论文描述了这项技术的历史和概况,而且读起来很有趣。Paul Graham 在他的书《On Lisp》的第 16 章也谈到了嵌套反引用,他说[2]
为了定义一个定义宏的宏,我们通常会要用到嵌套的反引用。嵌套反引用的难以理解是出了名的。尽管最终我们会对那些常见的情况了如指掌,但你不能指望随便挑一个反引用表达式,都能看一眼,就能立即说出它可以产生什么。这不能归罪于 Lisp。就像一个复杂的积分,没人能看一眼就得出积分的结果,但是我们不能因为这个就把问题归咎于积分的表示方法。道理是一样的。难点在于问题本身,而非表示问题的方法。

Bruno Haible 最近在 c.l.l.解释了避免嵌套反引用的两个不同方法。它们主要包含将嵌套反引用转换为简单反引用的做法。这两个方法他概述如下:

1. 在内层使用 LIST, APPEND 等函数代替反引用
2. 使用包含反引用的辅助函数

Bruno 为每个方法提供了例子。

首先,下面给出的这段代码用了嵌套反引用:

(defmacro once-only (variables &rest body)
  (assert (every #'symbolp variables))
  (let ((temps nil))
    (dotimes (i (length variables)) (push (gensym) temps))
    `(if (every #'side-effect-free? (list ,variables))
       (progn ,body)
       `(let
          (,,@(mapcar #'(lambda (tmp var)
                          ``(,',tmp ,,var))
                      temps variables))
          ,(let ,(mapcar #'(lambda (var tmp) `(,var ',tmp))
                         variables temps)
             ,body)))))

原来的代码使用方法 1 转换后如下:

(defmacro once-only (variables &rest body)
  (assert (every #'symbolp variables))
  (let ((temps nil))
    (dotimes (i (length variables)) (push (gensym) temps))
    `(if (every #'side-effect-free? (list ,variables))
       (progn ,body)
       (list 'let
         (list ,@(mapcar #'(lambda (tmp var)
                             `(list ',tmp ,var))
                         temps variables))
         (let ,(mapcar #'(lambda (var tmp) `(,var ',tmp))
                       variables temps)
           ,body)))))

原来的代码使用方法 2 转换后如下:

(defun construct-binding (variable form)
  `(,variable ,form))

(defun construct-let-wrapper (bindings body-form)
  `(let ,bindings ,body-form))

(defmacro once-only (variables &rest body)
  (assert (every #'symbolp variables))
  (let ((temps nil))
    (dotimes (i (length variables)) (push (gensym) temps))
    `(if (every #'side-effect-free? (list ,variables))
       (progn ,body)
       (construct-let-wrapper
         (list ,@(mapcar #'(lambda (tmp var)
                             `(construct-binding ',tmp ,var))
                         temps variables))
         (let ,(mapcar #'(lambda (var tmp) `(,var ',tmp))
                       variables temps)
           ,body)))))

两个方法中的任一个,产生的代码的可读性都比原来代码好得多。

作为 Bruno 建议方法的一种替代,也许你会考虑使用 Drew McDermott 的 BQ 反引用工具(包含在 YTools 中)。Drew 在 YTools 手册中对这个反引用问题有一个很好的解释:

反引用是 Lisp 的一个不可或缺的特性。然而在标准说明中它并非尽善尽美。我主要有两个不满:

  1. 当实现一个像反引用这样的机制时,需要 3 个东西:一个读取器,一个宏展开器,一个写入器。读取器将如`(foo ,x)的字符序列转换为如(backquote (foo (bq-comma x)))的内部形式(Allegro 就是这样读取的)。之后宏展开器将 backquote 调用转变为像(list 'foo x)构造函数形式。写入器将(backquote (foo (bq-comma x)))输出为`(foo ,x)

    不幸的是,Common Lisp 标准没有详细说明宏(macro)是什么。因此,它依赖于具体实现。对比来看,普通的引用(quote)有一个定义良好的内部形式(quote x),因此有一个定义良好的来自外部形式'x的转换。没有详细标准的问题就在于,要写一个你自己的工具配合读取器,宏取开器或写入器,是不可能的。例如,无法写一个可移植的代码遍历程序,针对反引用表达式来做一些特别的事情。事实上,一个 Lisp 实现甚至不要求有一个反引用的内部表示。读取器和宏展开器可以合并,以致于`(foo ,x)可以读作(list 'foo x)。此外反引用写入器的行为并非良好定义的,因为无法区别一个列表构造形式是不是由反引用转化来的。

    为什么要与宏展开器交互呢,这里有个例子。你或许也想和读取器有交互。假设你希望创造一个泛化的反引用读取宏(就叫它!@吧),这个宏建造一些并非列表结构的东西,你可以将(apply #'make-a-foo (list 'baz a) l)简写作!@(make-a-foo (baz ,a) ,@l)。当这个表达式!@(...)被读入时,很多 Lisp 实现会报一个类似“Comma not inside a backquote”(逗号出现在非反引用表达式中)的错,没有可移植的办法来干预读取过程来使它变成合法的。

  2. 用于阐明嵌套反引用的规则是,逗号是与围绕它的最内层的反引用配对的。(同时提取它的参数到上下文中,以便下一个逗号匹配到下一个反引用,如此类推。)

    我想这是错误的,或者至少在某些情况下是错误的。我从左到右地读反引用代码,因此先看见最外层的反引用。人们会喜欢这样理解:从反引用的角度来看,所有在它里面的东西,除了用逗号标记的之外都是“不会动的(inert)”(即被引用住)。对于所有可能会出现在其中的表达式,除了反引用外,这是成立的。所以如果你正编辑一个复杂的反引用表达式:

    '(foo (bazaroo '(fcn a ,x)))
    

    内层的引用不会“遮蔽”住x使它免于被求值。但如果将内层的引用改成反引用,那么遮蔽就正正要发生。你必须把它改成这样:

    '(foo (bazaroo `(,fcn a ,',x)))
    

    这个,',构造只是外观丑陋而已。它的唯一作用是将它的参数提取出最内层的反引用;你不能用,,x,因为这是说“当外层的反引用被展开时对x求值,得到e,然后当最内层反引用展开时求值e”。注意求值是次序是从外到内,然而嵌套反引用的规则是从内到外。非常,非常的令人迷惑。

这些不是巨大缺陷:99.9%的反引用不是嵌套的,而且几乎没人关心一个反引用的内部表示是什么。可是如果你感兴趣,文件bq.lisp提供一个可选的实现。

因此,正如 Common Lisp 里大多数的事物一样,如果你不喜欢某些东西,你可以改变它!当你需要写这类嵌套反引用的代码时,你有(至少)4 种不同的可以考虑的选择(基于你有多少嵌套反引用工作要做,还有你个人对代码风格/可读性的偏好):

1. 使用嵌套反引用
2. 在内层使用 LIST, APPEND 等函数代替反引用
3. 使用包含反引用的辅助函数
4. 使用一个反引用的替代实现

现在,出山吧,去写一些这样的代码:一种形式的代码不断的生成下一种形式,像生物进化的过程那样一代一代衍生。;-)

注:
[1] 即 Obfuscation Contest。国际 C 语言混乱代码大赛(IOCCC, The International Obfuscated C Code Contest)是一项著名的国际编程赛事。
[2] 此段译文引用田春(伞哥)等人翻译的《On Lisp 中文版》对应章节。

Emacs 修炼之道

“Don’t panic.”
— The Hitchhiker’s Guide to the Galaxy

作为一个 Emacs 控,在办公室我也积极的向别人推荐 Emacs,有时候会需要向别人介绍和解释 Emacs,比如它能够做什么,与其它编辑器有什么不同,如何使用它等。Emacs 是一个很大的话题,而且网上关于 Emacs/Vim/其他编辑器的各种圣战中飞扬的口水已经太多了,所以这里我把这篇内容缩小到 Emacs 的修炼这一点上,因为对于那些真正对 Emacs 感兴趣并准备起锚出发的 Emacs 初学者,如果有一个详细的指南,它记录了从入门到进阶等各个阶段,以及前进的道路上有哪些可以借力的资料或资源等,这是很有帮助的。于是我有了写此文的打算,作为一个过来人我就斗胆写一下 Emacs 的修炼之道。

一般来说,那些决定不用系统自带的文本编辑器,而专门花时间学习 Vim/Emacs 的人都需要在文本上做比较多的工作,他们希望在工具这一块上做长期的投资——即使 Vim/Emacs 的入门需要一些精力和时间,但熟悉之后能提高工作效率并长期发挥作用,当然这些人之中有很多是程序员。如果你像我认识的一些朋友一样,对系统自带的文本编辑器,比如 windows 下的 notepad,保持 100%的满意,那么就不用接着往下看了,尽可跳过下文并继续留在那个与世隔绝的默认编辑器的桃花源里,这种说法好像比较鄙视 notepad,但其实它主要是说:如果编辑需求复杂多变,那么编辑器相应的也需要变得复杂强大。

接下来这一个 Emacs 之道会有点长,因为 Emacs 被设计成能提供各种复杂多变的编辑功能,甚至是文本编辑之外的,例如玩游戏或收邮件等其它功能,所以 Emacs 才会有“伪装成操作系统的编辑器”这一别称,而且学习曲线会变得非常的,呃,奇怪[1]。尽管下文有点长,但是记得带上你的毛巾[2],无论遇到什么情况,最重要的是,Don’t panic。

我把 Emacs 修炼的道路分为几个主要的阶段:

1. 入门小徒

初学 Emacs,首先要了解 Emacs 的基本操作,Emacs 自带的 tutorial 文档就是为零起点的新手而写的,它介绍了窗口(window),文件,缓冲区等概念,并一步一步的指导初学者学习光标移动,插入修改,搜索,保存退出等基础功能,是最好的入门指南。默认的键绑定下在 Emacs 中使用 C-h t 可以打开这个 tutorial 文档,也有对应的中文译版叫《Emacs 快速指南》,文档不长,整个看完并按文档指引练习一下,之后就可以用 Emacs 完成简单的文本编辑。

接下来可以阅读《Learning GNU Emacs》,这本由老牌黑客 Eric Raymond 参与编写的书是学习 Emacs 的经典书目,内容很多也很全面,它介绍了从用 Emacs 做基本的文本编辑,到用 Emacs 收发邮件阅读网页,到定制编辑宏以及做程序开发等各种功能。初学者可以只读前 5 章,对 Emacs 的环境及常用操作做一个比较细致的了解。这本书的原英文第 1 版讨论的是 Emacs 的 18 版本,之后第 2 版出版,讨论的是 Emacs 的 19.30 版,而 2004 年出版的第 3 版,则随着 Emacs 的版本升级把内容更新到 21.3 版,到 2013 的今天,Emacs 的主版本号已经升到 24,虽然这本书在过去一段时间也算是与时俱进,但从版本上来远远落后于 Emacs 更新的速度,不过,很多内容在 Emacs 中保持稳定,还是很有价值的,只是阅读的时候要注意一下版本变化。这本书也有中文译本,已经绝版,但对应的只是较旧的英文第 2 版,译文流畅印刷错误也少,译本质量尚佳,因为是针对初学者而写的入门书,讲解详细,辅以图示截屏帮助理解,读来比较轻松。

进一步学习的话,则根据各人的需求而选择了,需求不同则学习的内容也不同。希望使用 Emacs 来做大量重复编辑的人可以学习定制编辑宏,用来写笔记的可以学习 outline-mode 或 org-mode,用来收邮件的可以学习 mew 等邮件客户端的用法。考虑到使用 Emacs 的人当中有很多的程序员,那么也根据编程语言的不同,学习的编辑模式也不同,Emacs 对于多种编程语言都有着很好的支持,如 C, C++, Java, Shell, Python, Perl, Erlang, Lisp……还有常见标记语言如 Html, XML 和排版用的 Tex, LaTeX 和 Troff 等。此外你可能会需要一些特别的功能,这些功能在其它的编辑器/IDE 也有,比如显示行号,在编写代码时的自动补全,全文格式化,代码折叠与展开,实时显示代码错误等,Emacs 还有一些比较独特好玩的功能,如 hungry delete。学习这些内容,除了《Learning GNU Emacs》这本书第 5 章后面的多个章节之外,网上也有很多的资料,不过就比较分散了,要找到 Emacs 是否有满足你特定需要的扩展,或者这些扩展怎么使用,google 是一个很好帮手。

这个时候你已经很熟悉 Emacs 日常使用,以及为自己的编辑需要加了一些扩展和定制,在配置文件不断变大的同时,你可能也知道一点简单的 Emacs Lisp 写法,但对 Emacs Lisp 还没有深入的了解。

由于各人的编辑需求不一样,这个寻找某种特定功能并定制使用的过程可长可短,了解 Emacs 在各种编辑模式下的功能有助于进一步学习 Emacs Lisp 及 Emacs 的内部实现,但这不是必需的。即使你很快的进入下一阶段开始系统学习 Emacs Lisp,但如果时不时会遇到新的编辑需要,还是会重复上述的扩展定制过程。以像我自己为例,因为需要编辑的代码种类越来越多,如后来的 Markdown, Erlang,所以也对应的添加了 markdown-mode, erlang-mode 的定制代码。

2. 登堂入室

在接触 Emacs 的过程中,很多新手都会注意到一个事实:Emacs 是用一种叫做 Emacs Lisp 的 Lisp 方言写的,如果不懂 Emacs Lisp 是无法进一步走入 Emacs 的殿堂的。所以在这里我把“开始系统的学习 Emacs Lisp”做为进入到第 2 阶段的标志。

在 Emacs 自带的 Info 文档中,对 Emacs 的描述是:

Emacs is the extensible, customizable, self-documenting real-time display editor.

可以看到,可扩展性,可定制性和自文档化是 Emacs 三个特别强调的特点,前两个特点或者其它编辑器也有,比如 Vim,但自我文档化这一点,可算是 Emacs 的一个独门绝技,是的,在 visual studio 中也有帮助系统,不过与 Emacs 的自文档化比起来,还是差得太远了,这方面可以比肩 Lisp 的编程语言不多,Python 是其中一个,自文档化与交互式编程结合起来,整个编程环境会让写代码的人相当舒服。

要开始 Emacs Lisp 之旅,最好的学习资料也是 Emacs 自带的文档,在 Info 中有一个 Emacs Lisp 的入门教程《An Introduction to Programming in Emacs Lisp》,这个文档由另一位老牌黑客 Robert Chassell 所写,由浅入深地详细介绍了 Emacs Lisp 中的列表操作,函数,缓冲区操作,窄化,剪切粘贴,正则表达式,代码调试等内容,由于把读者定位为没学过编程的人,所以行文比较啰嗦,程序员读起来可能会有点不耐烦。它也有中文翻译版,书名叫做《GNU Emacs Lisp 编程入门》,不过也比较旧了,是 2001 年由 FSF 中国组织翻译并出版的,年代久远,对应的 Emacs 版本是 19,这本中译版也已经绝版,不过网上可以找到电子版。虽然后来大师洪峰在他某次答问中说这本中译版质量很高[3],我个人看过纸版全书之后觉得其实翻译和出版的错误也不少,像他读过那么多书的人肯定清楚一个译本的质量怎么才算是“很高”,不过此书的中文翻译是他组织的,有点敝帚自珍也是人之常情。

特别值得一提的是,在这本书的前言里,作者 Robert 说“希望读者养成阅读源代码的习惯”,并把 Emacs 当成一个代码的宝藏,学习其工作机制。书中的内容多是具体的代码细节,看过之后是会忘记的,但“深入代码”这个习惯一旦养成,受用无穷。我个人认为也可以依据同样的标准来定义玩 Emacs 的程度,真正 Emacs 玩得好,不是说要用过多少个扩展,用得有多么的熟悉,而是深入到源代码去了解其内部实现。

在读完《GNU Emacs Lisp 编程入门》的基础上,可以找一些其它的 Emacs Lisp 入门级资料来看看,比如 wiki 上的 Emacs Lisp Cookbook,上面就有常用的各种数据结构,文件目录操作等的代码示例。经过学习之后,到网上一些站点,比如 Emacs 中文网李杀网,看一些文章通读其中简单的 Lisp 代码已经没有困难。

同时,可以阅读一些 Emacs 中常用小功能的源代码,比如自动高亮括号的 paren.el,高亮当前行的 highlight-current-light.el,显示行号的 linum.el,自动插入括号对的 skeleton.el 等,在读源代码的过程中有时会遇到未知的库函数和新的概念,通过查询 Emacs 的自带文档一般可以解决问题。

通过前面的学习,在前一阶段的基础上你已经掌握大量的 Emacs Lisp,在阅读源代码上有了不少的积累,但由于没有全面系统的学习 Emacs Lisp,有时候读起源代码来不懂的地方太多,会有种力不从心之感,这时候就可以开始进入第 3 阶段。

3. 武林高手

对于学习 Emacs Lisp,最权威的两个文档当然是 Emacs 自带的 Info 文档《The Emacs Editor》和《The Emacs Lisp Reference Manual》,前者主要介绍作为编辑器的 Emacs 概念和机制,比如 kill-ring,display, key-binding, 各种 mode 等,重点在使用,后者则全面系统的介绍了 Emacs Lisp 这门编程语言,比如各种数据结构及操作,宏,文加载等,重点在于编程,Emacs Lisp 作为 Emacs 的实现语言,当然两个文档会有标题或内容看起来很类似的章节,但由于两个文档重点不一样,两者互为呼应补充而非重复。这两个都是大部头,要读完的话甚费时日,但其内容并不艰深,读起来还算轻松,耐心读完的话,会功力剧增,对 Emacs 的理解有很大的提高。虽然我在这里把两个文档一起列出,但其实阅读顺序和阅读习惯可以按各人的习惯自由安排:可以先读前一本,读完再读后一本;也可以先读后一本,在遇到新概念的时候再回头看看前一本的对应章节。这两种都是比较有效的读法。就像电影《黑客帝国》第一部所讲的,在 Neo 知道 Matrix 不是看起来那样的真实之后,进入 Matrix 中能看到一些不真实的事物,读完这两本书之后,你再进入 Emacs 的时候,看着界面的 buffer,高亮的代码块,mode-line 等,不再只是看到它们显示出来的样子,而且透过表面看到了里面运行的模块和流动的代码。

作为读书的补充,可以适当的找一些源代码来看,一些常用的扩展如 auto-complete, yasnippet 等代码目录结构不算复杂的都合适。我个人建议以兴趣和熟悉程度为出发点,以你熟悉的 mode,或最感兴趣的扩展来作为开始,因为对外部功能了解越多,越有助于理解其内部代码,就像熟悉 Linux 的库和系统接口,有助于阅读内核源代码一样。

到此,你已经全面系统的学习过 Emacs 和 Emacs Lisp,在阅读源代码上也有了大量的积累。在正式变为高手之前,还有需要做一件事情:写至少一个扩展,并奉献出来。在开源文化或黑客伦理中,一个人成为高手或黑客,不仅因为他技术高超,而且因为他有很好的奉献精神。写一个扩展需要很多的劳动,写一个规模大而且质量好的扩展更是辛苦。虽然在前面的学习过程中,你已经读过一些扩展的源代码了,但在开始正式自己写扩展之前,或者你有兴趣阅读一下《Writing GNU Emacs Extensions》这本书,了解一下如何从头到尾写一个扩展。这本由 Bob Glickstein 写的书于 1997 年由 O’Reilly 出版,虽然内容很好,但是真的远远落后于时代了,对应的 Emacs 版本是 18,目前能找到的电子版印刷错误也较多。

4. 飞升成仙

到了这个阶段,进一步提升自己的方向有两个,一曰深度,一曰广度。

深度方面,可以全面阅读 Emacs 的源代码,学习其底层的 C 核心,Emacs Lisp 解释器的原理,学习 Emacs 的启动/运行过程,事件机制,与外部进程的接口,等等。还可以找一些较复杂较庞大的扩展代码来看,例如广为人知的巨兽 CEDET 等,除了熟悉如何用 Emacs Lisp 来实际编写大项目之外,还需要懂得很多与具体应用紧密相关的内容,例如对于 CEDET,就需要知道编程语言的词法文法解析过程。这种复杂项目对基础知识的要求较高,读起来较为艰深,需要做好“深挖洞,广积粮”的准备。

广度方面,可以广泛涉猎其它 Lisp 方言,学习 Lisp 的实现,历史及著名项目等,甚至可以弄一个 Lisp Machine 来玩玩,或者学习其它的函数式编程语言。这个话题太大,但我还是尽力尝试提供一点帮助信息。最大的两个 Lisp 方言当然是 Common Lisp 和 Scheme,如果详细叙述如何学习 Common Lisp 或 Scheme,那么可以需要写的篇幅比本文长得多,或者将来在我的博客上会出现《Common Lisp 修炼之道》或《Scheme 修炼之道》,但至少暂时是既无能力也无计划。这里只是简单列一下可供参考的书目:

《Structure and Interpretation of Computer Programs》,中译本《计算机程序的构造和解释》
《Ansi Common Lisp》,目前网上有在线中文翻译版本,前几章翻译得还不错
《Practical Common Lisp》, 伞哥(田春)翻译了中文版,2011 年出版,有的地方读起来比较生涩
《On Lisp》,伞哥和其他人合作翻译,网上有电子版,翻译质量很高,就像前文所说,“很高”这两个字不是随便用的
《Paradigms of Artificial Intelligence Programming: Case Studies in Common Lisp》,由 AI 大牛 Peter Norvig 所写的,书名看起来只是 AI 方面的书,但其实也是一本很好的 Common Lisp 教程
《Common LISP The Language, 2nd》,描述 Common Lisp 的语言标准

还有很多 Lisp 好书都没有一一列出,此外函数式编程语言也不少,虽然有时学习曲线陡得像是悬崖峭壁,但如果你真的到了这一步,相信会有足够的兴趣和技能支撑你继续走下去,总之可以选择的方向太多,这篇小文只谈 Emacs 修炼,适可而止。

到了这个层次,当然你也要注重发光发热,提挈后辈,可以在社区或邮件组多活动活动,奉献代码,解答疑问。不过这个也是大话题了,网上组织众多各人习惯不同,非三言几语能够说得清楚,按下不表。

如果以上提到的这些内容你都已经深入钻研,深到地球核心,又广到天际之远,那么恭喜恭喜,你可以正式宣称“神丹炼成,择日飞升”,到这个时候,你所需要的只剩下《颈椎病和鼠标手康复指南》。两千年前庄子就说了,“吾生也有涯,而知也无涯。以有涯随无涯,殆已;已而为知者,殆而已矣。”喜欢折腾 Emacs 的朋友们,人生短暂,还是悠着点吧。

注:
[1] 《主流文本编辑器学习曲线》这里有各种编辑器的学习曲线图示,Emacs 的曲线比较奇怪。
[2] 《银河系漫游指南》中毛巾这个词条有一些特别的解释。
[3] 这个问答后来被整理成为《你是如何成为 Lisp 程序员的》一文,在网上可以找到。