磨刀不误砍柴工
“工欲善其行,必先利其器”,工具的强是无敌的。而判断一个工具是否值得学习,需要理性的分析学习成本和收益。简单地讲,如果学习一个工具的时间远远超出你使用这个工具的时间,那么这个工具就是不值得你去学的。注意, 我并没有说这个工具本身不值得学,而是说它不值得你学 。同在互联网行业的人,你不可能建议 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,一并记录下来,希望后来者能够少走点弯路。
最后的最后,主流文本编辑器学习曲线,博君一笑。
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 my life"。
- 当然,Emacs 并不是完美的,体型巨大、启动速度慢、Elisp 的性能、多线程支持还有统一的扩展管理,这些一直被人诟病。
- 关于启动速度,最常见的优化方法有三种:
- 将 el 文件编译成 elc 文件,
- 将许多插件由
load
转换成autoload
。 - 在 Emacs 首次启动时开启
M-x server-mode
,然后以后启动 Emacs 只需要emacsclient
即可。我还做了几个懒人专用的 alias:alias ecc='emacsclient -c'
alias ecd='emacs --daemon'
alias ect='emacsclient -nw'
alias emacs='LC_CTYPE=zh_CN.UTF-8 emacs'
7
- Emacs 作为一个老牌自由软件,以无限的可扩展性作为核心竞争力,但直到近年来才出现了一些比较好的扩展管理工具,细节可以参考 ELPA: Emacs Lisp Package Archive、GNU Emacs的终极扩展管理工具。在此强烈推荐下 El-Get,结合 Eshell,让我在 Emacs 身上闻到了一丝 Lisp Machine 的味道。
- Eshell是可以直接调用 Elisp 函数的(这是我无意间发现的,惭愧),结合 el-get,使得 Emacs 扩展的安装可以像 debian 的
apt-get
一般简单。比如说,你可以通过如下的 Elisp 代码“一键安装” AUCTeX、auto-complete、CDLaTeX-mode、Slime、 YASnippet:
- Eshell是可以直接调用 Elisp 函数的(这是我无意间发现的,惭愧),结合 el-get,使得 Emacs 扩展的安装可以像 debian 的
(let ((pkgs '(auctex auto-complete cdlatex-mode slime yasnippet)))
(dolist (pkg pkgs )
(el-get-install pkg)))
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 测试,然后编译、运行(架屋叠床的设计8,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 的编程模式 9,不过要彻底理解这些,恐怕要涉及到对各种编程语言的深入探讨,目前的我功力有限,恳请高手不吝赐教。
- 不过像 Python/Ruby/Octave 这类语言,在 Emacs 里面开个文件同时开个解释器边写边测也是可以的,关键字:Comint Mode。
Common Lisp
- 和 C 语言不同,Common Lisp 的实现有很多10,我主要用的是 SBCL和 CCL,Arch Linux下的安装都比较简单,不再赘述。
- 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 文档,在 Arch Linux 中,你可以通过 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 lot11”。
差不多了,今天就写到这里,从早到晚写了一天了,累坏了,再写下去我估计读者也坚持不下来了。信息量太大,因此临时决定将文章拆成上下两篇,下篇我会谈谈 Scheme/Clojure 这两种 Lisp 方言开发环境的建立,并顺手谈谈 Emacs 和 Maxima 的集成。虽然 Maxima 本身并不是 Lisp,但是其基于 Lisp 实现的事实,也让其与 Emacs 的联姻充满了浪漫主义的色彩,最近在深入学习算法分析,常常用到 Maxima 和 LaTeX,十分快乐。敬请期待。
《The 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 那样到处是架屋叠床的类。问题域和实现域是我最近常常思考的问题,其深度超越于编程语言的范畴,后续我会再写文章深入探讨下这个主题。↩
"The only thing the emacs OS lacks is a really good editor",更多的八卦,这里。↩
坦白的讲,如果以击键次数为标准,单单比较文本编辑的效率,我认为 Vim 的效率确实比Emacs 强很多。考虑可扩展性的话,我认为 Emacs 的elisp还是要比 Vim 的vimscript 强很多的。↩
Living in Emacs,这篇 Emacs 之所以如此出名,完全在于它起了一个好名字,简明扼要的给出了这篇教程的终极目标。↩
这个主要是解决 Emacs 中文输入法的问题,细节可以参考解决 IBus 在 gVim/Emacs下不能使用的问题、还有输入法环境变量的故事。↩
架屋叠床这么有创造力的词来源于function/bind 的救赎,“尽管如此,Java还是沾染上了“面向类设计”的癌症,基础类库里就有很多架床叠屋的设计……”↩
All Common Lisp Implementations,伞哥的博客有很多关于 Lisp 极有价值的文章,他对 Lisp 的执着和不断学习的精神也让我很是景仰。↩