念念不忘,亦有回响

电影《一代宗师》佛前灯

多年以前,当我在 Linux 下选择 Emacs 做为编辑器的时候,似乎只是一个比较随机的选择。当时我已经接触并使用 VI 有一段日子,然而我使用它的体验并不算愉快,也许是我被 VI 命令模式和编辑模式搞乱了太多次,于是想找个其它编辑器试试,而当时我所知道的 Linux 终端字符界面编辑器并不多,Emacs 是其中一个,然后我就开始使用 Emacs,从此一发不可收拾,走上了一条不断修炼的漫漫长路。这段经历后来浓缩成了《Emacs 修炼之道》这篇文章。Emacs 不仅给我提供了一个舒适高效的编辑器工具与环境,也引领我走入了 Lisp 语言的世界。

Lisp 语言诞生自几十年前,这几十年间世事沧桑,一个又一个浪潮在计算机行业冒升又落下,一门又一门新的编程语言流行而又衰落,Lisp 语言早已退出流行的行列,然而却始终吸引着一小群程序员前仆后继地不断进入它的世界。除了传统的几种 Lisp 方言,2007 年出现的 Clojure 将 Lisp 在 JVM 平台重新做了一个现代的实现,也掀起了一波新的潮流,吸引了很多人的关注和使用。几十年前诞生的编程语言,大多早已消失在历史的长河中了,为什么 Lisp 能够保持长期的生命力呢?正如看待黑客文化时有人主要关注它的政治正确,有人则将它看成是嗡嗡作响的经济引擎,不同的人对 Lisp 优点的看法也大不相同。有人喜欢 Lisp 以 List 作为唯一代码组织方式的简单明了,有人欣赏 Lisp 由核心几个特殊 Form 支撑起来的体系,对应着数学领域里面的公理上搭建大厦的体系结构,有人则更实利化只关注可以帮助提高开发效率的那些方面。在我看来,Lisp 最吸引我的一点就是强大的元编程能力,非常便于编写 DSL。当然这并不是说其它方面不重要,如交互式编程,强调函数式风格但又不强制函数式为唯一风格等特点也是可圈可点。

Lisp 的思想和设计是如此优秀,然而传统的几种方言的实现 Scheme,Common Lisp 还有另外几个方言都没能紧跟这个时代的步伐,Clojure 的作者 Rich Hickey 必定是深知这点,才会创造了 Clojure。感谢 Rich,我们得以知道在某个语言的成熟 VM 上实现一个现代 Lisp 的可行性,也欣赏到了 Clojure 设计与哲学的种种优雅美丽之处。Clojure 设计于运行在 JVM 之上,固然是它的成功关键,因为如此一来就可以重用成熟的 JVM 环境以及丰富的代码库,同时 Java 程序员这个群体也非常庞大,能吸引到的 Clojure 使用者也多。另一方面也产生了一些缺点,因为底层的 JVM 原本是为 Java 设计实现的,而 Java 的并发模型还停留在操作系统进程/线程和锁这一种方式,因此 Clojure 除了保留 Java 的并发原语之外,还在并发上做了多种尝试,像软事务内存,几种不同的原语 delay, future, promise,还有几种不同的引用类型。如此多的新概念和原语不可谓不丰富,同时也是繁多复杂。每当我的脑细胞因为折腾可变与不变或者 write skew 而被烧死一部分的时候,我总是怀念 Erlang Actor 并发模型的简单。

既然要顺适多核化的趋势,就需要语言本身的并发机制支持用户态进程/协程。那么为什么不直接使用 Erlang 呢?虽然 Erlang 并不是 Lisp,但 Erlang 的设计也受到 Lisp 的影响,很多地方都有 Lisp 的影子,比如交互 shell,热更新等。然而我对 Erlang 也有觉得不满意的地方,除了经常被, ; .几个符号搞晕之外,最大的不满是来自于在 Erlang 里做元编程的不便,但是 Erlang 代码又有很强烈的元编程需求,比如 OTP 里面的大片模板代码。因此在编写 Erlang 代码的时候,我又相当怀念 Lisp 的 Macro。

那么,能否把 Erlang 和 Lisp 两者的优点结合起来呢,就像在 JVM 上实现 Clojure 一样,也许可以在 Erlang VM 上实现一门 Lisp 语言?到我的脑海中冒出这个想法的时候,距离我最早接触 Erlang 已经有三年多时间,距离学习 Clojure 已经有两年,另外当时在工作上我也写了大半年的 Golang,是的,近几年中我学习/使用过的编程语言也不少,基本上每年都会学一到两门新语言,虽然不是每个新学语言都有机会用来写大量的代码,但也算是见识了不少编程语言,也许是对很多好的语言有着更高的期望吧,没有哪种已知的编程语言令我觉得非常满意。带着把 Erlang 和 Lisp 结合的想法,我发现原来 Erlang VM 就像 Java VM,VM 之上也有中间语言层,可以基于这中间层来实现一门 Lisp,而且,早在几年前,就已经有人这样做了。我发现了 Robert Virding 写的 LFE,和 Eric Merritt 写的 Joxa。

作为参与设计 Erlang 的元老,Robert 一直以来也是探索 Erlang 应用方面的先行者,早在 2007 年就写了 LFE,并且影响了一众后来基于 Erlang VM 的新语言,如 Elixir。LFE 是 Lisp Flavored Erlang 的缩写,顾名思义,LFE 就是把 Erlang 写成 Lisp,除了多了一些括号之外(去掉了 Erlang 原来的分隔符如, ; .等)跟 Erlang 十分相似,LFE 的设计目标在于提供一个可扩展语法的 Erlang,它的实现也达到这个目标。然后跟我理想中的 Lisp 语言相去有一定距离,我并不喜欢它的 Lisp-2 风格,跟 Erlang 相似的模块设计如显式的 export,只在编译时可用的 Macro 等方面。

另外一个实现 Joxa 则是由 Eric 在 2011 年开始写的,Eric 也是 Erlang 界的老前辈了,他先是试用过 LFE,然后觉得并不如意才重新设计和实现了 Joxa,之前我写过的文章《Joxa: 一种基于 Erlang VM 的现代 Lisp 编程语言》详细描述了两者的渊源和差别,这里就不再展开了。总的来说,Joxa 的 Lisp 味相对更浓一些,更符合我的个人口味。在过去的一年多时间里,我仔细阅读了 Joxa 的实现并添加了 Erlang R17 后引入的 Map 的语法。Joxa 虽好,但有两个主要原因让我不得不考虑重新做一个实现:

  1. Joxa 目前的实现是用自举的方式将 Lisp 代码经过编译转换成 Core Erlang,自举是很 cool 的一种实现编译器的方式,然而用在语法不够稳定的场景下,会提高后续语言开发的复杂度和难度。Core Erlang 也是一个规模小巧设计良好的中间层,然而文档方面相当缺乏,很多时候必需花大量时间人手去获取从上层到它的翻译细节。相对来说更上层的 Abstract Format 虽然规模更大,然而文档较多,一些工具也只工作在这一层,所以更适合用于做为编译器的目标语言。在这一点上我和 Eric 讨论得出的一致结论是,要将 Joxa 用 Erlang 重写并将目标输出定位到 Abstract Format,却因为彼此都忙暂时未有足够精力将其重写。

  2. Joxa 的设计目标强调它要小而简,希望成为一个小的核心语言,如果其他人有需要就在其上定制扩展。这种设计也是 Lisp 语言的一种传统特色,由于扩展性强,可以做到核心小扩展大,类似的思想也被人在不同的语言和项目上实现,比如 PyPy 和 Python 3。然而我更希望有一套功能较多较全的编程语言,因此需要在语法和代码规模上都需要添加(或者改动,但主要是添加)很多内容,这就与 Joxa 的目标有冲突。据我所知 Eric 对 Joxa 有明确的定位,应该不会乐意在 Joxa 里面接受这些添加或改变,因此最好我还是用一个新项目名字来重写一个基于 Erlang VM 的 Lisp 实现。

一直以来我有个朦胧的想法,希望在下一个我能够自己决定命名的项目,能够用上一个带有我居住所在地中国华南地区的风物,或者带有东南亚色彩的名字,在给这个新项目取名时,老家旧居楼房前面的两棵高大的木棉树出现在我的脑海,那是一种在南方非常常见非常普通的树,平时不会特别觉察到它们的存在,然而有时觉察到它们的存在却又不禁觉得特别,像鲁迅笔下所说:“在我的后园,可以看见墙外有两株树,一株是枣树,还有一株也是枣树”。于是就将这个新项目的名字取为木棉,英文是 Kapok。

我想,Kapok 这个新的基于 Erlang VM 上的 Lisp 实现会包含如下这些内容:

  1. 一套类似 Clojure 风格的现代 Lisp 语法(Lisp-1),当然在语法上并不能无脑照抄 Clojure,比如方括号[]在 Clojure 中表示 Vector,在 Erlang 却是 List,那么 Kapok 里如何选择?若语义跟随前者,是用 Erlang 的 tuple 来实现 Vector 语义还是另外再做一套实现?若语义跟随后者,和普通圆括号()又如何区分,各自的使用场景如何确定?每处细节都需要仔细考量,这些细节也会影响其它方面,如下面将提到的兼容性。另外 Clojure 语法糖较多,而我不喜欢太甜。

  2. 与 Erlang VM 保持最大的兼容,在 Erlang VM 上实现的多种语言,都选择了将语法及语义尽量向 Erlang 靠拢,比如新语言的函数直接编译成 Erlang 的函数,如此得以保持良好的兼容性,新语言的代码可以直接与 Erlang VM 基础设施与已有库代码进行相互调用,不像 Clojure 那样需要通过中间包装。Erlang 是一门函数式语言,这意味着新语言最好也保持这种函数式的语义,当然也可以通过上层逻辑修改这种限制,比如另一门基于 Erlang VM 的语言 Elixir 通过在编译器做变量名映射的技巧使得在 Elixir 里同一个变量可以做多次绑定,如此一来 Elixir 在变量使用上更像命令式语言。基于两个原因我个人更偏向于在 Kapok 里面完全保留 Erlang 的函数式语义,一是它利于并发,二是它语义更清晰。

  3. 新的语言机制,目前已经在考虑中的有:支持 Clojure 的 Protocol(运行时类型分派,让语言更动态),探索一套带类型标注的 DSL 来做强类型编程(强制编译时类型检查,让语言更静态),Lazy API。其中第 Protocal 和 Lazy API 源自 Clojure(当然 Clojure 也借鉴了其它语言),并且已经在 Elixir 中得到实现。同时我对其它新想法持开放的态度。

  4. 功能丰富接口现代的标准库,包括常用宏,文件,网络等方面,特别一提的是 Erlang 原生用字符列表来表示字符串并不理想,因此需要有一整套高效易用的 unicode 字符串标准库,在 LFE 和 Elixir 中都重新定义了 binary string,这是一个比较好的方案,可以借鉴。

  5. 强大且现代的开发工具集,包括:文档及 Apropos 接口/工具,编辑器集成(如 Emacs mode), 项目管理工具(参考 Mix),交互性开发环境(集成 Slime 和 Swank)等

上面的几点综合了 Clojure, Erlang, Joxa, LFE, Elixir, Emacs 等多种语言或机制,希望可以取多家之长,提供一套强大完整的开发环境。当然也可以说是一个大杂烩,然而不是无脑照搬,库和工具集可以丰富多样,然而基础语法需要保持简约节制,避免出现 C++或 Scala 那样过于烧脑的设计。同时也要了解到上面几点内容只是一个初步的想法,后面可能会根据实际情况有所添加或改变。我一直有个用一门新编程语言编写一个开源数据库的朦胧想法,Kapok 完善到一定程度之后,也许可以用 Kapok 来完成我这个想法。

有了简要设计,就可以开工编写代码了,首先要实现的是 1, 2 两点,亦即核心编译器部分。要写 Kapok 的想法早在去年就已经产生,然而一直拖拖拉拉未能动手。去年年中祖母去世,让我明白到时间匆匆人生太短,有些事情想做就抓紧时间去做,迟早人生的终点只是青山上的一把黄土。于是开始动手,由于平时上下班剩余时间不多,生活琐事缠身,进展较为缓慢,拖拉了几个月逐渐完成了编译器前端,大概到今年初开始写后端,也算是慢慢重温了一遍大学时开的编译课程。最近有了一些时间,终于折腾到可以跑起来的程度。我想 Kapok 这个项目不可谓不大,终究不能短期内完成,要做到非常完善的程度,更是有待时日。就像长跑,并非重在一时之快慢,积跬步恒坚持,方能走得更久更远。我所需要做的,就是调整呼吸,跨开步子。

于是就把 Kapok 开源了,github 地址在这里,虽然暂时还有很多问题和缺点,也有很多想法未能实现,但我想这也算是一个不错的长跑起步。

十年之前,当我第一次安装并启动 Emacs 的时候,决没有料到会十年之后它带领我在 Lisp 的世界走了这么远。几年之前在接触 Erlang,Clojure 的时候,没有想到有一天会用 Erlang 重写一门 Clojure 风格的 Lisp 语言。一年之前开始着手 Kapok 的时候,我也不知道这个项目要做到何时,做到什么程度。然后事情冥冥中就这样发生了,而我将沿着这条路继续向前走。王家卫电影《一代宗师》里面,宫先生说“念念不忘,必有回响”,行知做人跟练武一样,秉着意念坚持下去,终究能有一点回响。

Comments