Emacs configs for a clojurian
俗话说,工欲善其事必先利其器,完善开发工具与我而言是一件快乐的事情,分享也是一件令人愉悦的事情,所以我想把学习过程中的点滴记录下,留作备忘。本文不会介绍太多花式或有深度的emacs配置,更多是摸索学习的过程,其中充满了乐趣。
原因
网络上的*.emacs.d/init.el配置数不胜数,各路lisp大神的dot file都已经放在github上了,而且前有牛人撰文推荐学习emacs配置的详实方法,看似确实没有什么必要自己折腾一份配置。这个说法对,也不对。我在转向emacs之前,是一名忠实的vim党,从大学开始就不断折腾vim的配置,还花过一段时间专门学习了vimscript,曾经惊叹于vimscript的动态函数式风格的优美和强大。类似地,.vimrc*配置文件在网络上也多如牛毛,华丽和酷炫的插件极大地提升了vim的操作性。尽管如此,我还是乐于一砖一瓦地打造自己的vim环境,竭力演化它变成我心目中的“编辑器之神”。这个过程一般会充满修改然后重启的重复性机械劳作,偶尔会遭遇无论怎么修改就是不生效、甚至遍寻google也一无所获的挫折,但是我就是无法厌倦它,人天生好奇,探索未知事物本身就充满了乐趣,而且一旦配置奏效,便能获得满满的成就感。新事物对程序员具有极大的吸引力,但是程序员不会止步于使用新事物,而且会在惊奇之余,渴望控制那股背后主导它的力量本身,行使“上帝之力”。
话说回来,为什么我会从vim党摇身一变成为emacs党呢?这就不得不提起Clojure这门lisp方言,出于对lisp和函数式编程的痴迷,我选择了基于JVM的Clojure作为自己的偏好语言,而emacs天生为lisp而生。有了这个充足的理由,我开始收集emacs的cheatsheet并打印出来,天天放在手边翻阅,甚至买了一本英文版的Learning GNU Emacs书籍,只要有机会就打开emacs开始刷4clojure上的编程题。由于emacs对lisp的亲和性,我几乎没花多少时间就掌握住了常用的操作技巧。不过,emacs最负盛名的学习曲线确实让学习者绕过圈子,只要一段时间不用,就会忘记很多基本操作。另外,为了更好地在emacs中编写Clojure,还需要cider-mode和clojure-mode的支持,这时候就不得不编辑init.el文件,本着KISS (keep it simple, stupid)原则,我照着各种插件的说明文档中,把配置项复制粘贴到init.el文件当中,运行起来没有问题就好。随着自定义的内容变多,init.el文件也急剧膨胀起来。膨胀本来算不上问题,但我是个比较有操守的程序员,臃肿的代码是我极力避免的坏味道(bad smell)。所以胸臆之中涌动一股浩然之气,决心学起emacs lisp,把emacs的配置从头来过。
从『头』开始
init.el文件位于*~/.emacs.d目录之下,如果没有,自行创建一份即可。
首先,我们需要用到emacs的包管理工具package.el*,因为emacs 24及其以上的版本都已经内置,所以无需下载到本地,直接通过require加载到emacs的运行时。
1 | (require 'package) |
上面的代码涉及到setq(变量赋值)的操作,package-archives,顾名思义,多个包的下载源,我给package-archives设置了5个包源,它们之间服从顺序的优先级,即先从第一个源中下载包,如果没有,到第二个原种寻找,以此类推。此外,这里("melpa" . "http://melpa.org/packages/")中的点号(dot)表示法也比较奇怪,其实这是lisp中的Dotted pair表示法,用法和普通的列表类似,但因为是pair的缘故,你可以使用(car )获取"melpa",(cdr )获取到的却不再是一个列表,而是"http://melpa.org/packages/" 这个值本身。
对emacs lisp不熟悉不要紧,先找个教程练习一下它的用法,比如learnxinyminutes就非常不错。完成这个教程,大体不会对elisp犯怵了。接下来,只需要使用c-h v和c-h f查看elisp中定义的变量函数就能很快上手自行配置。
来个实际的例子,在大牛的配置文件中,经常能看到如下成对的配置:
1 | (setq package-enable-at-startup nil) |
开始我觉得这是一对矛盾的配置,package-enable-at-startup设置为nil,暗示emacs启动时不会启用package,而package-initialize明显表明在做package的初始化工作。这种时候,我心中就蹦跶出一句话“世界上本没有矛盾,如果出现了,检查你都有哪些前提条件,就会发现其中一个是错的”。这种非异常的知识点很难通过搜索引擎找到满意的答案,而阅读文档恰恰是最合适的解决方式。emacs对elisp文档的支持非常全面,只需将鼠标移到package-enable-at-startup变量上,按下c-h v (control + h, v) 组合键,就能在其它窗口(window) 看到文档描述:
Whether to activate installed packages when Emacs starts.
If non-nil, packages are activated after reading the init file
and beforeafter-init-hook'. Activation is not done ifuser-init-file’ is nil (e.g. Emacs was started with “-q”).
意思是在读入init.el之后,这个变量才会生效。换句话说,在读取init.el的过程中,该变量不论是nil或是non-nil都不会影响package的加载和初始化。所以,这两者之间并没有矛盾。当然,此时你可能会想把package-enable-at-startup设置为nil意欲何为?官方文档中有如下的解释:
This will automatically set package-enable-at-startup to nil, to avoid loading the packages again after processing the init file.
简单点说,就是防止在package-initialize 之后重复加载包,因为可能会影响性能。
模块化
如果把什么东西都揉到init.el文件中,这个文件一定会很快变得臃肿不堪。为了解决这个问题,需要引入模块化的思想——把特定功能的配置放到独立的文件中,然后require进来。按照惯例,我在*~/.emacs.d目录下建立一个lisp目录用于存放所有自定义的模块文件,随后在init.el中加入下面这句代码,意在把lisp*目录加到emacs的加载路径列表里。
1 | (add-to-list 'load-path (expand-file-name "lisp" user-emacs-directory)) |
看似,接下来就可以在每个独立的模块文件中编写各种功能的配置。但是由于package.el功能的局限,我们很快就会遇到包重复安装和配置漂移(configuration drift)的麻烦。package.el提供了package-install-p(p是predicate的意思)和package-install两个配套使用的函数,也就是说一般得先判断包在不在,才决定安不安装。幸运的是,有人已经很好地解决了这部分问题,use-package就是非常好用的包,它将包的配置和包的定义聚合到了一块,并且保证包一定会安装在你的系统当中。
在使用use-package之前,我们需要先安装它,如下:
1 | (unless (package-installed-p 'use-package) |
由于use-package本身就是一个包,所以可以使用package-install安装到本地,然后require到emacs的运行时,值得一提的是这个eval-when-compile函数,使用c-h f查看它的定义:
Like ‘progn’, but evaluates the body at compile time if you’re compiling.
Thus, the result of the body appears to the compiler as a quoted constant.
In interpreted code, this is entirely equivalent to `progn’.
初次看到compile time,心中难免会有疑问:lisp不是动态语言吗,怎么还需要编译?这种时候,我们就要求助于elisp的文档了。在emacs中按下c-h i获取主话题(topic)的菜单,然后点击Elisp进入它的操作指南。重点查看Evaluation和Byte Compilation两个章节。不难发现lisp的解析器可以读取解析两种类型的lisp代码,一种是适合人类阅读的代码,以el作为后缀;另一种是编译字节码,以elc作为后缀。编译字节码运行速度优于前一种代码,我们可以通过byte-compile-file把前一种代码的文件编译成字节码文件。有趣的是,如果我们使用package来安装包,对应包的目录下都存在配套的el和elc两类文件。
在Byte Compilation条目下,有eval-when-compile的完整描述:
If you’re using another package, but only need macros from it (the byte compiler will expand those), then ‘eval-when-compile’ can be used to load it for compiling, but not executing. For example,
1 | (eval-when-compile |
这里头有三个关键字load、compiling和executing值得留意一下。为了弄懂它们的含义,我们需要了解lisp解析器基本的工作原理:code text -[characters]-> load -[lisp object]-> evaluation/compiling -[bytecode]-> lisp interpretor。换句话说,除非你想编译包含上述代码的文件,否则它的作用和progn一模一样,顺序地求值包含其中的表达式。当你正在编译文件的时候,包中宏就会原地展开,然后被eval-when-compile宏加载进内存并被编译成字节码,供后续解析器执行。
Clojure相关
载入use-package之后,我需要开始配置自己强大的Clojure开发环境了。首先,引入几个包:
1 | (use-package rainbow-delimiters |
rainbow-delimiters能够让括号变得如同彩虹一样绚丽(主要是易于区分forms),clj-refactor是重构Clojure程序的神器,company提供了强大的命令补全提示功能。
clojure mode
接下来,我们在*~/.emacs.d/lisp目录下新建一个init-clojure.el*文件,内容如下:
1 | (require 'clj-refactor) |
这里就能看出use-package的好处来了,针对clojure-mode的配置项都统一放到:config中管理起来。配置完毕后,使用(provide 'init-clojure)将模块以这样的名字暴露给其它客户端调用。
CIDER mode
有了clojure-mode之后,我们还需要一个Clojure可交互式的开发工具,CIDER便是这么一款工具。同样地,我们在lisp目录下新建一个名为init-clojure-cider.el,内容如下:
1 | (require 'init-clojure) |
配置的首部,我使用(require 'init-clojure)先加载init-clojure,然后对CIDER本身进行一系列的配置。配置的详细信息可以通过CIDER github主页获取到,这里我就不再赘述。
最后,需要在init.el文件中添加入这么一句(require 'init-clojure-cider),重新启动emacs,找到一个Clojure项目,按下C-c M-j (hack-jack-in),就能获得一个Clojure的交互式开发环境。
小结
当然,我的emacs配置绝对不止这些,但是其余的过程大体类似。由于emacs速来有伪装成编辑器的操作系统的称号,所以我的探索是无止境的。如果大家对我的配置感兴趣,可以直接去我github上dotfiles上查看。
—
参考链接
[1] sriramkswamy dotemacs
[2] purcell emacs.d