Clojure(Script) 的编程乐趣

Clojure 绝对不是“另一门”编程语言,它足够的老,可以追溯到 1972 年,又足够的现代,不论是前端的消息总线还是后端的不可变数据库。它不依赖重型的框架,提倡简单就好的理念,面向不可变的数据开发。它足够的优雅和有趣,以至于 Uncle Bob 选择 Clojure 作为他二三十年职业生涯最推崇的选择。

我第一次接触 Clojure 是在 2020 年的春节,当时正在啃 “计算机程序的构造和解释” —— 俗称 CSAPP 这本书。CSAPP 使用一门叫做 Scheme 的方言教学,为了最大化学习 Scheme 的价值,我便想着找一门类似的 JVM 方言,这样既学习了语法,又能实际在工作和学习中用到,岂不美哉?可惜的是,深冬季节脑袋不转圈,加上春节琐事不少,所以 CSAPP 这本书就暂时搁置了,但凭借着“掌握一门 LISP 方言”的虚荣,我还是囫囵吞枣的读完了《Clojure 编程乐趣》这本书,还做了笔记,这算我和 Clojure 的第一次接触。

作为浸淫在 C 风格语法的程序员是很难一下子上手一门 LISP 方言进行开发的,这不同于 Go 或者 Dart 或者 Kotlin 这种设计者会考虑 C 风格语法亲和力的语言。在整个 2020 年,前 6 个月在忙着论文答辩,后 6 个月也在忙着论文答辩,因此也没什么机会去试试手。这种情况一直持续到 21 年底,我终于又捡起来了 CSAPP,花了一两个月读完后,之前很多稀里糊涂的想法一下子变得清晰起来,仿佛打通了任通二脉,对于软件工程的理解深刻了很多。在这种情况下,出于“最幸福的程序员莫过于在公司写着 LISP 拿着薪水”的虚荣,考虑到现实的情况,掌握 Clojure 的事便又提上了日程。

按照 “Clojure for the brave and truth” 一书作者的话,穿越 LISP 表达式和宏的高山,跨越 FP 惯用法和标准库的沼泽,终于到达了 Clojure 生态的丛林。我花了一周重读了《Clojure 编程乐趣》,然后更新了笔记。这时候正好在工作中有开发特性对接口的需求,所以用 Clojure 写了点程序。我还清楚的记得自己写的第一个 Clojure 程序是用来调用系统命令执行 maven 编译,然后将包按照命名规则 scp 到测试服务器,这个程序随后不断更新,支持了自动 git 脏 maven 模块检查,集群替换包等功能,最后借助 Clojure REPL 还搞了一个 SHELL 出来。第二个 Clojure 程序是用来检查微软商店 OneNote 是否有更新,然后 Slack 通知我更新的版本。这个时候我接触到了 ring,踏上了 Clojure Web 开发的道路,逐步实现了 mock 开发接口,分享开发和接口文档,最后添加了自动生成 TR 特性文档,评审文档,Swagger 接口和 UI 的功能。这些小脚本被我打包放在一个叫做 devKit 的模块中,用来简化重复的日常工作。

Clojure SHELL

Clojure Web 服务

我还记得使用 ring 写这个 Clojure Web 应用时那种兴奋感 —— 在我熟读 Spring 源码,使用 Scala Play 开发过异步、纯函数式的多个程序,写过不少 Django 和 Go 的 Web 服务后,Clojure 带来的感觉完全不同,一切都非常熟悉:Jetty,Servlet,一切却非常陌生:一切都是 map,包括请求和返回值,没有任何变量、数据结构不可修改,比 JavaScript 更快的动态热加载:当焦点从 IDE 离开,新的方法和接口就已经准备就绪。这是一种符合心智模型的纯粹:不可变值,不可变数据接口,甚至不可变的数据库,数据在随意组合的简单函数中穿梭和流动。如果说有什么可以形容 Clojure,优雅和乐趣一定是让人耳目一新的标签。

这套 devKit 的系统并非是一个“传统”的 Web 应用,它并不遵循“数据输入 - 处理 - 数据输出”的模型,没有明确的“数据”和“代码”界限,就好像任何 LISP 表现出来的一样,所有的数据在 .clj 源代码文件中被定义,都是简单的 Map 字面量和 Vector 字面量,以及关键字和字符串。换言之,我使用 .clj 写格式化的文档,碰巧它也可以被 Clojure Reader 读取并编译成 JVM 字节码和可识别的数据结构并通过 Web 提供数据服务:渲染到 DOCX 或者 PDF 或者 HTML 模板中。

很轻松的,这套系统被用来实现 mock 接口,并且彻底改变了我的开发工作流:

我急切的到处吹嘘这套开发工作流,在某次被总经理听到后,他觉得可以探索推广的可能性,提高整个部门的开发效率。这个时候我才仔细的考虑这套流程 —— 本质上就是结构化数据,有了数据之后,建立在其上的程序是轻易的,可以自动化和简化的动作也是更加丰富的。但关键是,如何让多人生产这些结构化的数据?这就需要一套用户界面,一堆表单来提供数据的录入。

陆陆续续的,我读了《Clojure Web 开发》这本书以及《PostgreSQL 即学即用》,掌握了 Clojure 的 JavaScript 实现 - ClojureScript,并且接触了 Clojure 的前端生态系统。如果说 Clojure 在 JVM 上算的是具有特色的语言 —— 毕竟面临着 Scala 和 Kotlin 的竞争,那么 Clojure 在 JavaScript VM 上则充分保持了先进性。React.js 最早借鉴 ClojureScirpt 的不可变数据结构进行的重构,才有了现在的 React 哲学,而现在的 ClojureScript 则支持几乎无缝的通过 shadow-cljs 和 npm 类库进行交互。ClojureScript 的 React 实现 Reagent 比 React 本身更像 React,ClojureScript 的事件总线 re-frame 也比 Reduce 要简洁、灵活和先进的多,当然,大部分原因是 React 决定让自己成为一个类库而非框架,因此主要的差异在于语言的语法:不巧的是 Clojure 和 JavaScript 作为类似的 LISP 方言(广义)上,JavaScript 没有任何表现能力上的优势。

结果就是,借助于 PostgreSQL 后端和 ClojureScript 的 Reagent 编译到 React 编译的 JavaScript 前端,很快就做出来了一个用来展示数据,录入数据的交互能力丰富的 Web 应用:

随着“让结构化数据掌握在自己手中才能发挥更大用处”这种信念加深,我又做了一个简单的博客,用来记录一些开发的资源和文档。在慢慢的迭代过程中,逐步添加了一些特性,比如语法高亮,TODO 支持,图片大小限制。

当然,为了重复利用这套 CSS 布局和现成的代码,在 2022 年的春节,花了几天做了一个简单的“物品管理系统”,意在简化每次外出确定拿那些东西的心理负担。以及 —— 结构化数据以备后续的需求。

上述两套系统的 Clojure(Script) 代码量合计不过五六千行,其中大部分是用于 HTML 布局的语法,此外是 ClojureScript 前端交互和事件传递的代码(三四千行左右),后端 API、服务代码很少,甚至还没有 SQL 多。总的来说,作为后端开发来说,深刻体会到了想要实现一个自然的、用户易用的同时简洁的交互是多么困难,此外还需要考虑布局在手机、平板和大小屏幕显示器上的显示效果问题。

但不管怎样,我总算是在 Clojure(Script) 上实现了“全栈开发”的梦想。总的来说,Clojure 绝对不是“另一门”编程语言,它足够的老,可以追溯到 1972 年,又足够的现代,不论是前端的消息总线还是后端的不可变数据库。它不依赖重型的框架,提倡简单就好的理念,面向不可变的数据开发。它足够的优雅和有趣,以至于 Uncle Bob 选择 Clojure 作为他二三十年职业生涯最推崇的选择。

我已经记不得有多少次写 SQL 忘记加逗号,写 Java 总想去 partial 和 map 一个数据结构,Clojure 及其短暂的在我的职业生涯中留下了难忘的记号,改变了我对于编程的思考。从来没有掌握了 Clojure 而不喜欢它的,那么本文唯一的目的就是:如果作为读者的你希望掌握一门充满乐趣的优雅的表达力丰富的编程语言,那么 Clojure 将是一个很好的选择。希望看到每个开发者的创造力都因为 Clojure 而更加丰富,做出更好玩、有趣的产品。