一名Clojurian的Emacs配置

我是一名热衷于函数式编程的Clojurian(Clojure粉),网络ID是lambeta(λβ),读作/‘læmeitə/,个人的博客网站是https://lambeta.com。俗话说,工欲善其事必先利其器,完善开发工具与我而言是一件愉快的事情,所以想把经验集结成文字,便有了这篇文章。这篇文章不会介绍太多花式或有深度的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的运行时。

(require 'package)
(setq package-archives '(("melpa" . "http://melpa.org/packages/")
                         ("melpa-stable" . "https://stable.melpa.org/packages/")
                         ("marmalade" . "http://marmalade-repo.org/packages/")
                         ("elpy" . "http://jorgenschaefer.github.io/packages/")
                         ("gnu" . "http://elpa.gnu.org/packages/"))
      package-enable-at-startup nil)

上面的代码涉及到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 vc-h f查看elisp中定义的变量函数就能很快上手自行配置。 来个实际的例子,在大牛的配置文件中,经常能看到如下成对的配置:

(setq package-enable-at-startup nil)
(package-initialize)

开始我觉得这是一对矛盾的配置,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 before after-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的加载路径列表里。

(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之前,我们需要先安装它,如下:

(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

(eval-when-compile
  (require '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进入它的操作指南。重点查看EvaluationByte Compilation两个章节。不难发现lisp的解析器可以读取解析两种类型的lisp代码,一种是适合人类阅读的代码,以el作为后缀;另一种是编译字节码,以elc作为后缀。编译字节码运行速度优于前一种代码,我们可以通过byte-compile-file把前一种代码的文件编译成字节码文件。有趣的是,如果我们使用package来安装包,对应包的目录下都存在配套的elelc两类文件。 在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,

(eval-when-compile
   (require 'my-macro-package))

这里头有三个关键字loadcompilingexecuting值得留意一下。为了弄懂它们的含义,我们需要了解lisp解析器基本的工作原理:code text -[characters]-> load -[lisp object]-> evaluation/compiling -[bytecode]-> lisp interpretor。换句话说,除非你想编译包含上述代码的文件,否则它的作用和progn一模一样,顺序地求值包含其中的表达式。当你正在编译文件的时候,包中宏就会原地展开,然后被eval-when-compile宏加载进内存并被编译成字节码,供后续解析器执行。

Clojure相关

载入use-package之后,我需要开始配置自己强大的Clojure开发环境了。首先,引入几个包:

(use-package rainbow-delimiters
  :ensure t)
(use-package clj-refactor
  :ensure t)
(use-package company
  :ensure t
  :defer t
  :config (global-company-mode))

rainbow-delimiters能够让括号变得如同彩虹一样绚丽(主要是易于区分forms),clj-refactor是重构Clojure程序的神器,company提供了强大的命令补全提示功能。

clojure mode

接下来,我们在~/.emacs.d/lisp目录下新建一个init-clojure.el文件,内容如下:

(require 'clj-refactor)
(require 'rainbow-delimiters)

(use-package midje-mode
  :ensure t)

(defun my-clj-refactor-mode-hook ()
    (clj-refactor-mode 1)
    (yas-minor-mode 1) ; for adding require/use/import
    (cljr-add-keybindings-with-prefix "C-c C-m"))

(use-package clojure-mode
             :ensure t
             :config
             (add-hook 'clojure-mode-hook #'rainbow-delimiters-mode)
             (add-hook 'clojure-mode-hook #'subword-mode)
             (add-hook 'clojure-mode-hook #'midje-mode)
             (add-hook 'clojure-mode-hook #'my-clj-refactor-mode-hook)
             (add-hook 'clojure-mode-hook #'enable-paredit-mode))

(provide 'init-clojure)

这里就能看出use-package的好处来了,针对clojure-mode的配置项都统一放到:config中管理起来。配置完毕后,使用(provide 'init-clojure)将模块以这样的名字暴露给其它客户端调用。

CIDER mode

有了clojure-mode之后,我们还需要一个Clojure可交互式的开发工具,CIDER便是这么一款工具。同样地,我们在lisp目录下新建一个名为init-clojure-cider.el,内容如下:

(require 'init-clojure)
(require 'company)

(use-package cider
             :ensure t
             :config
             (setq nrepl-popup-stacktraces nil)
             (add-hook 'cider-mode-hook 'eldoc-mode)
             (add-hook 'cider-mode-hook #'rainbow-delimiters-mode)
             ;; Replace return key with newline-and-indent when in cider mode.
             (add-hook 'cider-mode-hook '(lambda () (local-set-key (kbd "RET") 'newline-and-indent)))
             (add-hook 'cider-mode-hook #'company-mode)
             (add-hook 'cider-repl-mode-hook 'subword-mode)
             (add-hook 'cider-repl-mode-hook 'paredit-mode)
             (add-hook 'cider-repl-mode-hook #'company-mode)
             (add-hook 'cider-repl-mode-hook #'rainbow-delimiters-mode))

(provide 'init-clojure-cider)

配置的首部,我使用(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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏极客生活

真正好用的python库

这个库是我安装完python环境后第一个安装的库,装上这个库再开始写代码才有底气,作者 Kenneth Reitz 是公认python领域代码写的最好的两个人之...

16930
来自专栏Kirito的技术分享

天池中间件大赛百万队列存储设计总结【复赛】

维持了 20 天的复赛终于告一段落了,国际惯例先说结果,复赛结果不太理想,一度从第 10 名掉到了最后的第 36 名,主要是写入的优化卡了 5 天,一直没有进展...

25030
来自专栏Android 开发者

开发者也是用户 - 第二部分:改善 UI 和 API 可用性的五条指导原则

20530
来自专栏机器学习和数学

[编程经验] 我是如何半自动抓取素材公社图片的

网络爬虫是一件比较繁琐的事情,特别考验人的耐心。但又是非常令人着迷的一件事,因为当你从网络上爬到了自己的想要的数据,满满的成就感油然而生。但是我对爬虫掌握的并不...

36450
来自专栏北京马哥教育

Linux 的 OOM 终结者

现在是早晨6点钟。已经醒来的我正在总结到底是什么事情使得我的起床闹铃提前了这么多。故事刚开始的时候,手机铃声恰好停止。又困又烦躁的我看了下手机,看看是不是我自己...

46560
来自专栏陈本布衣

Spring基础篇——DI/IOC和AOP原理初识

前言   作为从事java开发的码农,Spring的重要性不言而喻,你可能每天都在和Spring框架打交道。Spring恰如其名的,给java应用程序的开发带了...

26670
来自专栏AI派

早上起床后不想动,让 Python 来帮你朗读网页吧

之所以用 Python,就是因为 Python 有着丰富的库,网页正文识别也不在话下。这里我尝试了 readability、goose3。

22520
来自专栏tkokof 的技术,小趣及杂念

小话游戏脚本(一)

( 题记:近来在网上学习到一个新的观点(应该是来自刘未鹏的BLOG :) ):书写是为了更好的学习,这与之前脑子里传道授业解惑的观点颇为迥异,品一品又颇以为然...

11310
来自专栏北京马哥教育

Linux | 只有十三个字符的经典Fork炸弹,讨厌谁就去整他吧!

Jaromil在2002年设计了最为精简的一个Linux Fork炸弹,整个代码只有13个字符,在shell中运行后几秒后系统就会宕机: 这样看起来不是很好理解...

49360
来自专栏编程

C语言怎么学习?以C语言模拟登录为例,学C语言该当如此方可成

好了不用看了,看直了也不会有妹子,看题了: C语言编写代码实现,模拟用户登录------看着是不是很简单?,是不是很轻松?,是不是就有这样的代码? (小编随便百...

44390

扫码关注云+社区

领取腾讯云代金券