行者无疆 始于足下 - 行走,思考,在路上

走进Lisp的世界——兼谈Emacs下Lisp的开发环境(上)

1 磨刀不误砍柴工

”工欲善其行,必先利其器“,工具的强是无敌的。 而判断一个工具是否值得学习,需要理性的分析学习成本和收益。简单地讲,如果学习一个工具的时间远远超出你使用这个工具的时间,那么这个工具就是不值得你去学的。注意, 我并没有说这个工具本身不值得学,而是说它不值得你学 。同在互联网行业的人,你不可能建议UI设计师去学习Emacs/Vim,也不太可能去要求码农去学习Photoshop。

编程几乎是一种纯脑力劳动,更确切的说,编程是人脑的一种思维运作。 高效的编程必然伴随着顺畅的思维运作,任何使你在编程中感觉到思维运作受阻的东西,都是你需要不断改进的东西,包括但不限于编程语言、算法和编程工具。 我在前面的的文章《少即是多——兼谈对SNS的看法》 也曾经写到:“深入的思考是容不得别人打扰的,一旦中断,思考的大厦就会崩塌,重建的过程往往循环往复、困难重重。这就是为什么聪明人只想和更聪明,至少是和自己一样聪明的人一起工作的原因,资深的Hacker更是如此,他们才没有耐心告诉你Apache该怎么配置呢。”

简单的例子,作为码农,如果你不能流畅的阅读英文技术资料,那么你需要去提高下英文能力,考个TOFEL/GRE或许是个一劳永逸的办法;如果你的电脑配置不能顺畅的运行你需要用到的开发工具,那么攒钱换电脑或者增强配置吧, 时间就是金钱,将珍贵的时间耗费在等待软件启动的过程中,这是对生命的一种亵渎 ;如果你不能顺畅的实现各种基本数据结构和算法乃至于无法深入看一些技术书籍和自我学习深造,那么找本算法书吧,认认真真的将所有代码从头敲一遍,最好再做一些习题;如果你觉得C++的各种特性杂糅使你不堪重负,使得你在解决问题的时候不断地需要脱离问题本身而去关心实现细节的,那么你应该考虑下去学习几门新的编程语言,跳出语言的框架去寻找解决方案1

除了以上种种,最常见也最容易被广大码农忽略的,就是高效的文本编辑。很多人对此不以为然,鼓吹能力是最重要的,工具是次要的,甚至举出某某牛人用记事本编出某某牛软件的例子来自我麻醉,为自己的懒惰和不思进取提供理论支撑和YY的对象。后面我会说明,语言和工具对于编程工作而言起着至关重要的作用,如果将来有朝一日我去招人,我第一件事肯定问他是Emacs/Vim用得是否熟练,对于Emacs/Vim都没有听说过的人,我肯定是坚决不会要的。

为什么高效的文本编辑如此重要?因为对于码农而言,最重要的工作就是写代码, 而写代码本身可以看成是一种特化的文本编辑工作 ,因此找一款看着舒心、用着称心的编辑器是很有必要的。我不止一次的想起,大三时光,某个实验室的角落,某某同学滚着鼠标寻找码海里的某个函数定义。他的滚轮每滚一格,我的心就咯噔一下;每次他滚了一大段又往回滚的时候,我的心就咯噔咯噔跳个不停。何必呢?即使你不知道emacs/vim,不知道source insight这样的工具,但你至少应该会Edit->Find吧。《卓有成效的程序员》 里面有几条非常重要的原则:

  • Using the mouse less.
  • Prefer typing over mousing.
  • Typing is faster than navigation.

总结起来,这三条原则的核心要旨就是快,快速定位到你想要到的地方,随心所欲,不为外物所阻。毕竟,“天下武功,无坚不破,唯快不破“。只有快,才能让思维的高速列车略偏方向而迅速纠偏,不至于偏离车道,走向“车毁人亡”的不归路。具体说来,我们编程是为了解决问题(思维的高速列车),但是很多时候我们不得不花时间和精力去处理诸如代码缩进、括号配对等非常琐碎的工作(偏离方向),而这些琐碎工作不仅会严重影响我们思维的顺畅性,更有甚者,它有时候会让我们忘了我们最初需要解决的问题(“车毁人亡”了)。这里有一段yasnippet的demo视频, 是一个极佳的高效文本编辑的例子。比如我们写html代码,我们真正关心的问题应该是到底选用那个标签,至于这个标签该怎么缩进怎么配对怎么符合xhtml标准都不是也完全不应该是我们需要分心解决的问题;日常工作中诸如此类的例子还有很多很多。所有琐碎的小问题加起来,足以碎化你的思维,让你举步维艰。“没有NFS、Java和其他的技术还能活;但是如果没有Vi,简直没法活了”2,可见一个高效文本编辑器在优秀程序员心中的地位。

废话好像有点多,接下来主要谈谈Emacs下Lisp开发环境的配置,几天的折腾碰到了很多大大小小的issue,一并记录下来,希望后来者能够少走点弯路。

最后的最后,主流文本编辑器学习曲线, 博君一笑。

2 Emacs

  • ”Emacs是伪装成操作系统的编辑器3(emacs isn't a text editor but more of an operating system that incidentally happens to include a text editor.)“。
  • 没错,Emacs就是一个操作系统,只是这个操作系统本身缺少类似于Vim这样高效的编辑器4。如果你去看下Emacs和Elisp合起来将近2000页的手册,你就会发现,emacs这货真不是一个编辑器这么简单,实在是一个以编辑器为核心构建起来的操作系统,有自己的编程语言(elisp),API,API文档,window、frame……
  • 很多人说Emacs反Unix哲学的,因为Unix的哲学是提供简明的接口(这个接口主要是文本流),透过小工具的组合完成所有的工作。但Emacs似乎野心太大,妄图以一己之力完成从编程、上网、邮件、听音乐等所有的工作5。从某种意义上而言,这种说法是正确的。但是换一个角度,如果我们真的将Emacs看成一个类似于Unix这样的操作系统的话,那么Elisp之于Emacs就相当于C之于Unix;Emacs的各种插件扩展就相当于Unix下的各种小工具;Unix通过Shell管道将各种小工具粘合起来完成复杂的工作,而Emacs通过自己的Elisp环境将各种扩展插件整合起来,让他们完美合作,完成各种各样对编辑器而言几乎不可能的工作。从这个意义上而言,如果你真的把Emacs当做一种平台而不仅仅是一个编辑器的话,那么Unix的哲学和Emacs是不冲突的。因为Unix的哲学针对的是工具,而不是工具底层的平台,不是吗?
  • 可能你会问,Emacs到底能干些什么?Easy,除了Adobe能干的,Emacs都能干。具体而言,Emacs不擅长做图形图像视频音频方面的工作,这方面是Adobe CS系列软件的天下。而除了这些基于图形图像的工作, 其余 基于文本流的工作,Emacs都能干的不错 6
  • 如果你想看看Emacs到底长什么样,emacser.com一定会让你大开眼界。
  • Ruby语言的创始人松本行弘在LibrePlanet 2012 conference上讲述了"how emacs changed mylife"。
  • 当然,Emacs并不是完美的,体型巨大、启动速度慢、Elispe的性能、多线程支持还有统一的扩展管理,这些一直被人诟病。
  • 关于启动速度,最常见的优化方法有三种:
    • 将el文件编译成elc文件,
    • 将许多插件由load转换成autoload。
    • 在Emacs首次启动时开启M-x server-mode,然后以后启动Emacs只需要emacsclient即可。我还做了几个懒人专用的alias:
      • ecc='emacsclient -c'
      • ecd='emacs –daemon'
      • ect='emacsclient -nw'
      • emacs='LC_CTYPE=zh_CN.UTF-8 emacs'7
  • Emacs作为一个老牌自由软件,以无限的可扩展性作为核心竞争力,但直到近年来才出现了一些比较好的扩展管理工具,细节可以参考ELPA: Emacs Lisp Package ArchiveGNU Emacs的终极扩展管理工具。在此强烈推荐下el-get,结合eshell,让我在Emacs身上闻到了一丝Lisp Machine的味道。
    • eshell是可以直接调用Elisp函数的(这是我无意间发现的,惭愧),结合el-get,使得emacs扩展的安装可以像debian的apt-get一般简单。比如说,你可以通过如下的elisp代码“一键安装”auctex、auto-complete、cdlatex-mode、slime、yasnippet8
(let ((softs '(auctex auto-complete cdlatex-mode slime yasnippet)))
  (dolist (obj softs)
    (el-get-install obj)))

3 Slime

  • 学习计算机四年有余,用过的编程工具IDE环境没有上百也有一打,但从来没有任何一种编程环境,能够像Slime那样,让我印象深刻,彻底颠覆我的编程方法学和世界观。
  • 这种颠覆型的编程模型就是slime的交互式编程。
  • 多数人都已经对C/C++/Java这种编译型语言的构建模型见怪不怪了,对于C++ Template这种扭曲的所谓元编程模型和超长的编译时间也学会了忍耐,大不了去上个厕所、抽一颗烟,要么就去泡杯咖啡呗。可是很少有人去深入思考过,为什么我们要忍受冗长的编译过程?为什么我们只是随便更改几句代码,就要重新做一次完整的编译?如果你从来没有思考过这些问题,那么请尝试下Slime吧,或者python/ruby也好的,交互式的编程会颠覆你的编程理念。
  • Paul Graham在它的《Ansi Common Lisp》用这样一段话来描述Lisp中的编程模型:"In purely functional code, you can test each function as you write it. If it returns the values you expect, you can be confident that it is correct. The added confidence, in the aggregate, makes a huge difference. You have instant turnaround when you make changes anywhere in a program. And this instant turnaround enables a whole new style of programming, much as the telephone, as compared to letters, enabled a new style of communication"
    • 我认为这段话强调的关键之处在于"instant turnaround", 即快速的修改和反馈,更加生动和详细的描述可以参考Paul Graham的另一本Lisp广告书《黑客与画家》。
    • 想快速构建一个链表一棵树?没问题,在lisp中这些都可以用大一统的list来表示的。Alan J. Perlis在SICP的序言中曾写到:"It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures"。如果你认真用C/C++/Java实现过链表和二叉树,你会发现两者的数据节点声明是一样的,都是一个data域和两个指针域。为什么会这样?很少有人深入想过这个问题。后续我会写文章,从Lisp的角度上探讨下这个问题。
    • 想快速测试某个函数的正确性和性能?没问题,开启slime然后C-c C-c即可,你再也不用像Java那样,先建立一个类、然后声明一个static function,最后在写JUnit测试,然后编译、运行(架屋叠床的设计9,OOP的风格也许并没有声称的那么美好)。Alan J. Perlis在SICP的序言中还写到:"Pascal is for building pyramids—imposing, breathtaking, static structures built by armies pushing heavy blocks into place. Lisp is for building organisms—imposing, breathtaking, dynamic structures built by squads fitting fluctuating myriads of simpler organisms into place."
  • 关于Slime配置,如果你直到什么叫load-path、major-mode、mode-hook这些elisp概念的话,还是比较容易的。要么就只能照抄网上配置碰运气了。Understanding SLIME (Using Emacs and Lisp Cooperatively)是一篇极好的Slime资源,Quick Intro to Live Programming with Overtone令人印象深刻,极为震撼。
  • python/ruby这类动态语言可以用Slime吗?这点我没有找到太好的资料,slime的contrib目录里面有一个ruby文件,但是我目前还不会ruby,所以没有做过尝试;google上搜到的一些资料说python由于语言本身的限制并不能采用Slime的编程模式10,不过要彻底理解这些,恐怕要涉及到对各种编程语言的深入探讨,目前的我功力有限,恳请高手不吝赐教。
    • 不过像python/ruby/octave这类语言,在Emacs里面开个文件同时开个解释器边写边测也是可以的,关键字:comint-mode

4 Common Lisp

  • 和c语言不同,Common Lisp的实现有很多11,我主要用的是SBCLCCL ,ArchLinux下的安装都比较简单,不再赘述。
  • Quicklisp 是推荐的Lisp库管理工具,Quicklisp之于Common Lisp相当于cpan之于Perl.
  • 在Emacs中装好Slime后(推荐用el-get),将下列代码放入SBCL的初始化文件.sbclrc或者CCL的初始化文件ccl-init.lisp中。启动SBCL或者CCL开启swank,然后在Emacs slime中用M-x slime-connect连接即可(swank可以是远程机器)。
;;; The following lines added by ql:add-to-init-file:
#-quicklisp
(let ((quicklisp-init (merge-pathnames "quicklisp/setup.lisp"
                                       (user-homedir-pathname))))
  (when (probe-file quicklisp-init)
    (load quicklisp-init)))

;;; swank for emacs slime to connect
(load "~/.emacs.d/el-get/slime/swank-loader.lisp")
(swank-loader:init)
(swank:create-server :port 4005 :dont-close t)
  • LispWorks公司为Common Lisp提供有一份非常详尽的HyperSpec 文档,在ArchLinux中,你可以通过AUR来安装(yaourt -S cl-hyperspec)。
  • Slime对HyperSpec提供了良好的支持:slime-hyperspec-lookup。配置好Emacs-w3m,就可以在Emacs通过w3m查询Common Lisp语言文档的,很方便。我的配置片段如下:
(add-to-list 'load-path "~/.emacs.d/el-get/emacs-w3m")
(require 'w3m-load)
(setq browse-url-browser-function 'w3m)

;; view common lisp hyperspec documentation
(global-set-key "\C-ch" 'slime-hyperspec-lookup)
(setq common-lisp-hyperspec-root "file:/usr/share/doc/HyperSpec/")
  • M-x slime-connect之后,几个常用的功能:
    • C-c C-c: slime-compile-defun,编译当前光标所在处的表达式
    • C-x C-e: slime-eval-last-expression,对last-expression进行求值
    • M-.: slime-edit-definition,这条命令可以看到Common Lisp中的各种语言结构诸如defun、and、progn的源码,代码取决于你所用的Lisp实现,非常强大,是深入理解Lisp底层的良师益友。
  • 绝大多数Lisp实现均支持trace函数,可以用来跟踪递归过程,形象化地展示递归的运行机理,是深入学习理解递归的良好工具。比如下面的SBCL的REPL中的代码展示:
CL-USER> (defun just-return (n) (if (zerop n) 0 (+ 1 (just-return (- n 1)))))

JUST-RETURN
CL-USER> (trace just-return)

(JUST-RETURN)
CL-USER> (just-return 5)
  0: (JUST-RETURN 5)
    1: (JUST-RETURN 4)
      2: (JUST-RETURN 3)
        3: (JUST-RETURN 2)
          4: (JUST-RETURN 1)
            5: (JUST-RETURN 0)
            5: JUST-RETURN returned 0
          4: JUST-RETURN returned 1
        3: JUST-RETURN returned 2
      2: JUST-RETURN returned 3
    1: JUST-RETURN returned 4
  0: JUST-RETURN returned 5
5
CL-USER> 
  • 书的话,伞哥的博客 已经给出了很好的建议,我再加一本Paul Graham的《Ansi Common Lisp》,一本一本的看吧。“LISP is worth learning for a different reason — the profound enlightenment experience you will have when you finally get it. That experience will make you a better programmer for the rest of your days, even if you never actually use LISP itself a lot”12

差不多了,今天就写到这里,从早到晚写了一天了,累坏了,再写下去我估计读者也坚持不下来了。信息量太大,因此临时决定将文章拆成上下两篇,下篇我会谈谈Scheme/Clojure这两种Lisp方言开发环境的建立,并顺手谈谈Emacs和Maxima的集成。虽然Maxima本身并不是Lisp,但是其基于Lisp实现的事实,也让其与Emacs的联姻充满了浪漫主义的色彩,最近在深入学习算法分析,常常用到Maxima和LaTeX,十分快乐。敬请期待。

--

Footnotes:

1The Joy of Clojure》有这样一段 话:”Writing code is often a constant struggle against distraction, and every time a language requires you to think about syntax, operator precedence, or inheritance hierarchies, it exacerbates the problem. “任何反紧凑的语言,其繁杂的 语言特性往往会使得人们在解决问题的过程中脱离问题本身而陷入语言细节的泥沼,要么是 像C++那样到处是坑到处是禁忌到处是编程规范,要么是像Java那样到处是架屋叠床的类。 问题域和实现域是我最近常常思考的问题,其深度超越于编程语言的范畴,后续我会再写文 章深入探讨下这个主题。

2 http://www.techcn.com.cn/index.php?doc-view-132647.html

3 "The only thing the emacs OS lacks is a really good editor",更多的八卦, 这里

4 坦白的讲,如果以击键次数为标准,单单比较文本编辑的效率,我认为Vim的效率确实比 Emacs强很多。考虑可扩展性的话,我认为Emacs的elisp还是要比Vim的vimscript强很多的。

5 Living in Emacs, 这篇Emacs之所以如此出名,完全在于它起了一个好名字,简明扼要的给出了这篇教程的终 极目标。

6 不擅长干并不代表不能干,比如 这里这里、 还有 这里

7 这个主要是解决Emacs中文输入法的问题,细节可以参考 解决ibus在gVim/Emacs下不能使用的问题、还有 输入法 环境变量的故事

8 鉴于天朝网络的奇葩性,如果某些扩展无法安装,无妨追查下是否是网络问题。解决方案关 键字:ssh && proxychains。

9 架屋叠床这么有创造力的词来源于 function/bind的救赎, “尽 管如此,Java还是沾染上了“面向类设计”的癌症,基础类库里就有很多架床叠屋的设计……”

10 Why there is no SLIME for Python (or Ruby or…)?

11 All Common Lisp Implementations,伞哥的博客有很多关于Lisp极有价值的文章,他对Lisp的执着和 不断学习的精神也让我很是景仰。

12 How to Become a Hacker

 




Host by is-Programmer.com | Power by Chito 1.3.3 beta | © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee