另存
你以为你在修改一个对象。其实你只是让一个名字,指向了另一个值。
值不动,动的只有名字——这句话吃透了,并发、撤销、时间旅行、Git、React、数据库的快照读,会同时变成同一件事。
这本书只讲一件事
在 Java 里,list.add(x) 之后,那个 list 变了。所有拿着它的人手上的东西都变了,
而他们不知道。你为此付出的代价,你已经付了很多年:防御性复制、unmodifiableList、
锁、volatile、「这个对象线程安全吗」、以及那些只在线上出现的诡异 bug。
Clojure 的回答不是「小心一点」,是把这件事从根上拿掉:值不会变。
(conj v x) 给你一个新向量,旧的那个一个字节都没动。
你的第一反应应该是:那不是每次都要复制一份吗?那得多慢?
这本书的第 2 章就回答这个问题,用一台真的持久化向量:100 万个元素的向量改掉一个,
整棵树 32,258 个节点里只新建了 4 个,其余 32,254 个在内存里是同一批对象。
顺便说,它比复制一遍数组快 600 多倍。
值不动
VALUES先把一件事看清楚:你从来没有改过任何东西。你只是让一个名字指向了另一个值。
手上的活
HANDS把语言本身过一遍。你会发现要背的东西少得离谱——因为大部分你已经会了。
懒
LAZY序列不是集合,是一份「要的时候再算」的承诺。而这份承诺的条款你多半没读。
时间
TIME并发不难。难的是你把「一个会变的东西」和「它此刻的值」当成了同一样东西。
扩展
EXTEND你早就撞过这堵墙:想给一个不是你写的类加个方法。这一卷讲四种撞法。
落地
REAL把前面五卷换成手上的活:一台不重启的 REPL、一份能生成数据的合同、一张账单。
写给谁
- 写过几年 Java 或 Kotlin,知道
ConcurrentModificationException是怎么来的, 也写过防御性复制。 - 没碰过 Lisp,一看见括号就有点想跑。这本书假设你的 Lisp 基础是零。
- 不需要你打算转去写 Clojure。这本书里有一半的东西, 你明天就能用在手上那个 Kotlin 项目里。
这本书不打算做的事
- 不劝你换语言。第 23 章会老实列出它真实的短板: 启动慢、错误信息难读、招人难、动态类型在大项目里的确要还债。
- 不做语法速查表。要查 API 请去官方文档,这本书讲的是「为什么长成这样」。
- 不吹「函数式更优雅」。凡是有代价的地方,这本书会把代价写出来, 包括分块惰性序列会多算 31 个元素这种事。
怎么读这本书
- 只有一小时:读第 1、2、16 章。这三章是主线的三个落点—— 值不动、不动为什么不慢、以及它在并发上意味着什么。
- 想上手写:读完卷 I、卷 II,然后直接去第 21 章那台 REPL 里敲。 它是真的,不是录像。
- 被并发折磨过:卷 IV 可以单独读,四章都有能跑的模拟器。
- 每一章的结构是一样的:开头「▷ 先猜一下」给你一道四选一, 结尾「◇ 另存一版」揭晓答案,并且明确写出你进来时的想法(旧值)和出去时的想法(新值), 以及这一章只重建了哪一条路径——这个栏目本身就在演示结构共享。
书里的数字是从哪来的
这本书配了一台引擎(assets/engine.js):一个真的 Clojure 读取器、求值器、
持久化向量与 HAMT 字典、分块惰性序列、转换器、STM、协议与宏。它不是「Clojure 风格的库」,
是照着 clojure.lang 的实际实现写的。
为了确认它没在骗你,仓库里还有一台探针(scripts/probe-clj):
它在真的 Clojure 1.12.4 上跑 470 行源码,把每一行的求值结果记下来,
再用 Java 反射走进真 PersistentVector 和 PersistentHashMap 内部,
按对象同一性数出「共用了几个节点、新建了几个」。
本书引擎必须对同一行源码给出逐字符相同的输出, 节点数也必须逐个相等。所以书里出现的每一个数字, 要么是引擎当场算的,要么是从真 Clojure 里量出来的——没有一个是我编的。