继续往下走
最后一章有四件事:一张自查表、这门语言真实的短板、往下读什么,以及最重要的一件——这本书里有多少东西,你明天就能用在手上那个 Kotlin 项目里,而完全不需要换语言。
最后一道题,关于这本书本身。
前面 23 章讲了持久化数据结构、惰性、转换器、STM、协议、宏、REPL。 问:如果你不打算用 Clojure 写任何生产代码, 这本书里有多少内容对你还有用?
一、24 条自查表
每一章有一条「✗ 这个直觉是错的」。全部收在这里,顺序和章节一致。 点开一条,看看你现在的回答和进来时是不是同一个:
二、这门语言真实的短板
这本书从头到尾答应过不吹。最后一章要把账结清。 下面这些不是「小瑕疵」,是可能让你的团队做出「不用它」这个正确决定的理由。
1. 错误信息很难读。这是最常被抱怨的一条,
而且抱怨得有道理。一个拼错的键名,可能给你一串几十行的栈,
指向 clojure.lang.RT 里的某处,
而真正的问题在你三个函数之前写错的那个 :custmer。
1.10 之后有改善(spec 化的错误信息),但和 Kotlin 的编译错误没法比。
2. 启动慢。JVM 加上 Clojure 运行时的初始化, 一个「hello world」要一两秒。这让它不适合命令行小工具和 传统的 serverless 短函数。缓解方案有 GraalVM native-image、 Babashka(一个启动即用的 Clojure 脚本运行时),但都有各自的限制。
3. 动态类型的债,在大项目上是真的。 第 22 章说过,spec 补不上编译期全覆盖那一块。 项目大到几万行、团队多到十几人时, 「改一个函数签名,不知道有谁在调」这件事会持续消耗你。 这不是可以靠纪律完全抵消的成本。
4. 生态小,招人难。库的数量和质量都远不如 Java/Kotlin 生态。 好消息是 Java 互操作非常顺,你可以直接用 JVM 上的任何库; 坏消息是那些库的 API 是为可变对象设计的,用起来总有点别扭。 招人这件事更实际:会 Clojure 的人少,愿意学的多,但入职后有爬坡期。
5. 工具链和主流不一样。 REPL 驱动很强,但要配;调试器支持不如 Java; IDE 的重构能力弱得多(没有类型,很多重构不安全)。
6. 官方节奏保守。spec 2 停摆多年、 核心团队对社区提案回应慢,这在 Clojure 社区里是长期存在的不满。 语言本身极其稳定(十几年几乎没有破坏性变更)—— 这是优点,也意味着别指望它跟进新潮流。
- 数据处理、ETL、分析类的活——这是它的主场。
- 需要在运行中不断探索数据的场景(REPL 的价值最大化)。
- 并发密集、状态复杂的服务端。
- 团队小而强,愿意换工具链。
反过来,如果你的项目是「大团队 + 长期维护 + 需求相对固定」, Kotlin 那套静态类型 + 成熟工具链大概率是更理性的选择。 这不是妥协,这是就事论事。
三、不换语言,你能带走什么
这是这一章最重要的一节,也是开头那道题的答案:C。
下面每一条都可以明天就用在你的 Kotlin / Java 项目里:
1. 把要一起变的东西放进同一个值。(第 16 章)
// ✗ 会撕裂
class State { var a = 0; var b = 0 }
// ✓ 不会
data class State(val a: Int, val b: Int)
val state = AtomicReference(State(0, 0))
state.updateAndGet { s -> s.copy(a = s.a - 10, b = s.b + 10) }
这一条的收益最大,代价最小。data class +
AtomicReference 就够了,不需要任何库。
2. 局部可变,边界不可变。(第 23 章)
函数内部随便用 ArrayList 求速度,
返回时转成 List.copyOf()。你已经在用 StringBuilder 做这件事了。
3. 副作用挪到重试之外。(第 14 章)
任何「读—改—写」的循环里,别放发邮件、写日志、发消息。
这条对 compareAndSet、数据库乐观锁、
@Retryable 一样成立。
4. 先用 map,需要派发行为时再建类型。(第 6 章) 在 Kotlin 里这条要打折——静态类型的价值是真的—— 但至少可以少建一半的 DTO:那些只是为了搬运数据、 没有任何行为的类,往往可以不要。
5. 认出「你以为在改它,其实是换了个指针」。(第 1、3 章) 这条不产生代码,产生的是判断力:看到一个 setter, 问一句「谁还拿着这个对象」;看到一个共享的可变集合, 问一句「这是身份还是值」。
6. 数据 > 函数 > 宏。(第 20 章) 在 Kotlin 里就是数据 > 函数 > 注解处理器 / DSL。 能用一个配置 map 表达的东西,别写成一个注解处理器。
7. 用「能不能」而不是「是不是」来设计扩展点。(第 17、18 章) 下次设计一个需要被第三方扩展的接口时, 问一句:使用方能不能给一个不是他写的类型接上我的能力?
- Compose / React:不可变状态 + 重算, 就是第 2、16 章。你已经在用了。
- Kotlin Flow / RxJava:转换器那一章的思路,
map/filter独立于数据源。 - Room / SQLDelight 的事务:第 15 章。
- Kotlin 的
copy():第 2 章的手工版本 (只是没有结构共享,每层都真的复制)。
你并不是在学一门新语言的特性, 你是在给你已经在用的那些东西找到共同的根。
四、往下读什么
如果你想真的写 Clojure:
- Clojure for the Brave and True(免费在线)—— 最好的入门书,语气轻松,覆盖完整。
- Programming Clojure(Alex Miller 等)——更系统,适合有经验的开发者。
- 官方 Getting Started +
deps.edn。工具链先用官方的,别一上来纠结选型。 - 编辑器:VS Code + Calva 是门槛最低的组合。 先把「发送表达式到 REPL」这个快捷键练熟,第 21 章的价值全在这里。
如果你想理解这套思想的来源:
- Rich Hickey 的演讲。这是我最想推荐的一项, 胜过任何一本书:Simple Made Easy(区分「简单」和「容易」, 影响了整整一代人)、The Value of Values(这本书的主线)、 Are We There Yet?(第 3、16 章的时间模型)、 Hammock Driven Development(怎么思考问题)。 都能在网上找到。
- 论文 Ideal Hash Trees(Phil Bagwell, 2001)—— 第 2 章那棵 32 岔树的原始论文。
如果你想往更深处走:
- Datomic:把这本书的主线做成一个数据库—— 数据库本身是一个值,时间是一等公民。即使不用,读它的设计文档也值。
- core.async:CSP 风格的并发,和 Go 的 channel 同源。 转换器(第 12 章)最初就是为它而生的。
- ClojureScript:同一门语言编译到 JavaScript。
re-frame是一个把「状态是一个值」贯彻到底的前端框架, 值得读源码。 - Malli:比 spec 更活跃的规格库(第 22 章)。
有争议的地方,你会听到不同说法:
- 动态类型够不够用。这是社区内外最大的分歧, 双方都有大型生产项目做论据。听两边的,别信任何一边的绝对化说法。
- STM 是不是失败了。它确实没被广泛使用 (第 15 章说过,多数人一个 atom 就够)。 有人认为这证明了它不实用,也有人认为这恰恰说明它把问题解决得太好了, 以至于你根本走不到需要它的那一步。
- 宏该用多少。社区共识是「尽量少」, 但边界在哪儿,每个团队的答案都不一样。
《快照》(Git):第 2 章的结构共享,那本书里是 tree 对象复用。 两本合起来看,你会看见同一个数据结构策略在两个领域被独立发明。
《同时》(并发):卷 IV 的正面补充。 这本书讲「怎么让需要小心的地方变少」,那本讲「小心的地方该怎么做对」。
《求值》与《解释》: 第 4、19 章的底层。亲手造一台解释器之后,宏就不再神秘了。
《上游》(Haskell):另一条路上的答案。 同样是不可变,Haskell 用类型系统把「必须是纯的」变成编译错误, 而 Clojure 用文档和纪律。对照着看,你会更清楚每一种选择在买什么。
《借还》(Rust):第三条路。 它既不放弃可变,也不靠垃圾回收, 而是用所有权规则在编译期证明「同一时刻只有一个人能写」。 三本书是同一个问题的三个答案。
「学 Clojure 就是学一门新语言。学完之后如果不用它, 就等于白学。」
这本书从头到尾只讲了一件事,而它不属于任何语言:
把「会变的东西」拆成「一串不变的值」加「一个指针」。
接受这一句之后,第 1 章那张跨领域对照表里的每一行都会自己长出来: Git 的 commit 与分支、数据库的 MVCC、React 的一帧、 撤销栈、事件溯源、区块链、你的记忆。 它们不再是九个要分别记住的机制,是同一句话的九个实例。
而这,正是这本书唯一想留给你的东西。语言只是它最集中的一个例子。
答案是 C:大部分内容在任何语言里都成立, 只是要你自己动手——上面第三节那七条就是清单。
A 「不用这门语言就用不上」——那样的话这本书不值得写。 真正只属于 Clojure 的东西其实很少:宏的具体写法、REPL 的具体工作流。 其余的都是可移植的判断力。
B 「思路能借鉴但落不了地」——落得了地。
data class + AtomicReference 这一条,
今天下午就能改。
D 「可以直接调用 Clojure 的数据结构」—— 技术上确实可以(那些类就是普通的 JVM 类, Vavr、Immutables 也提供了同类实现), 但这不是重点。重点从来不是那个数据结构,是那句话。
你翻开这本书时:修改一个对象是编程的基本动作; 不可变很安全但很慢;并发很难,要靠加锁和小心。
你合上这本书时:修改从来不是基本动作, 产生新值才是;不可变正是「不用复制」的原因; 并发的难处不在「同时」,在于你把身份和值当成了同一样东西。
你原来会的东西一个都没丢—— Java、Kotlin、你写过的每一行代码、你调过的每一个 bug,全都还在原地。 这本书只重建了从根到那几个节点的一条路径。 其余 32,254 个节点,你和过去的自己共用。
这本书的一句话
你以为你在修改一个对象,其实你只是让一个名字指向了另一个值。 值不动,动的只有名字。
就这一句。前面 23 章都是它的推论, 而你在这本书之外遇到的下一个「咦,这不是刚讲过的吗」, 多半也是。
——《另存》完