卷 VI · 造语CH 25深度 25/25

回流:带回 Kotlin 与 JS 的十二件事

我们绕了很远。从括号出发,一路走过闭包、续延、宏,最后亲手造了一门语言。现在全部收回来。你学这本书,从来不是为了转去写 Lisp —— 是为了带着一副新的眼睛回到 Kotlin 和 JS。这最后一章是你的行囊清单:一张把 Racket 概念对到你每天工作的对照表,十二件明天就能用的东西,一份自检清单,以及那个问题的答案 —— 接下来该读什么。

回流对照表自检延伸阅读

一张总对照表

这本书讲的每一个 Racket 概念,在你的语言里都有个影子。这张表是整本书的索引 —— 从「你每天在用的东西」反查「它的原理在第几章」:

你每天在 Kotlin / JS 里用的它的真身
lambda 捕获变量、闭包泄漏、Compose 的 rememberUpdatedState环境链:捕获的是盒子不是值05
高阶函数、inline、策略模式的消失函数是一等值;λ 演算06
&& 短路、?:、日志白算参数、orElse vs orElseGet应用序 vs 特殊形式07
tailrec、JS 第 7811 层爆栈、深递归改循环尾调用;栈就是续延08
sealed class + when、写代码前先想数据形状结构化递归;设计配方09
fold / reducesumOfgroupByfold 是列表递归的模式10
判别联合、「让非法状态无法表示」、Redux action代数数据类型 + 模式匹配11
Sequencefunction*StreamFlow惰性求值;正规序的手动模拟12
回调地狱、throwreturnawait续延:「接下来要做的事」13
生成器、异常、协程「感觉」不一样却是一回事call/cc;一个续延三种面孔14–15
Continuation<T>、suspend 状态机、协程取消规则CPS 变换;续延的编译器版15
「别 mutate state」、Compose 的 @Immutable、React 引用相等引用透明;不可变换廉价变更检测16
KSP、注解处理器、Babel plugin、编译器插件宏:展开期的代码变换17–20
Kotlin DSL、Compose、Gradle DSL、内部 DSL 的天花板#lang;语言就是模块23
require / check、zod、Bean Validation、防御性检查契约;带 blame 的规格24
规则引擎、公式求值、查询 DSL、配置里的逻辑元循环求值器:一个小 eval21–22

十二件能立刻用的东西

对照表是索引,这一节是行动项。不是「理解了」,是「下周就改」:

◆ 工作方式(1–3)

1. 反过来用 REPL。先在 Kotlin Scratch File / Node REPL 里把逻辑跑通,再搬进项目。你按「编译运行」的次数会掉一半。(第 2 章)

2. 用你语言的「Macro Stepper」。Kotlin 的 Show Bytecode → Decompile、TS Playground 的输出栏 —— 看编译器真的把你的 data class / async 变成了什么。用它的人和不用的人不在一个层次。(第 2 章)

3. 卡壳时,关掉编辑器,先写数据的形状。「这个函数怎么写」难,常常是因为「我在处理什么形状的数据」还没想清。先答:有几种情况?每种有什么字段?哪个字段是递归的?(第 9 章)

◆ 写代码(4–8)

4. when 永远当表达式用,不当语句。只有作表达式时 Kotlin 才强制穷尽;加子类型时编译器会把所有该改的地方标红。这是能写进团队规范的一条。(第 9、11 章)

5. 数一数你的状态数。设计 data class 时,数「类型能表示多少种状态」和「业务上合法多少种」。差额就是未来 bug 的储量。互斥的可空字段 → 改用 sealed。(第 11 章)

6. 认出 fold,但别炫技。「初始化变量→循环更新→循环后用」就是 fold。但如果标准库有更准的名字(sumOf groupBy associateBy),用那个 —— 名字是给读代码的人的。(第 10 章)

7. 默认不可变,可变就圈起来。对外暴露只读接口,可变留在函数内部且不逃逸。遇到「我改了 A,B 怎么变了」第一反应是别名。(第 16 章)

8. 需要惰性时才用 Sequence。它的收益来自「省下无用功」,所以只在「有大量无用功可省」(提前退出、大集合、长链)时划算;小集合上它的 thunk 开销反而更贵。别在它的 lambda 里放副作用。(第 12 章)

◆ 设计与推理(9–12)

9. 该延迟求值的参数,别设计成普通参数。问一句「调用者是否可能希望它不被求值?」是 → 用 lambda 参数(require(c){...}getOrElse{...})。Optional.orElse 的坑就是没这么做。(第 7 章)

10. requirecheck 分清 —— 它们是 blame。require = 怪调用者(参数),check = 怪自己(状态)。异常类型本身在告诉未来的调试者「往边界哪一侧看」。(第 24 章)

11. 在信任边界上把契约做厚。公共 API、模块接口、服务入口、解析外部数据 —— 这些地方的运行时校验(zod、require、Bean Validation)收益远超开销,还判定了责任方。(第 24 章)

12. 评估元编程方案时,爬那张成本阶梯。能用函数就别上元编程;需要延迟求值用 inline+lambda;需要生成代码才上 KSP;需要改语义才上编译器插件。每往上一格,先确认真的必要 —— 复杂度会传染给每个读你代码的人。(第 20 章)

一份自检清单

这本书真正想给你的,不是十二条规则,是四个能解释掉你职业生涯里大多数「见鬼了」的问题(第 1 章那个提问姿势的最终版)。遇到诡异行为,按顺序问:

⟲ 遇到「这怎么会这样」时,问这四个问题

① 它什么时候被算的?
是不是我以为没算的东西其实早算了(应用序、急切求值)?还是我以为算了的其实没算(惰性、短路、宏推迟)?— 第 7、12、17 章

② 它在哪个环境里算的?
这个变量指的是哪个盒子?有几个盒子?谁还记着这个盒子?— 第 5 章

③ 它算完之后去哪了?
这个值接下来交给谁?谁在等它?这个续延什么时候、在哪个线程被恢复?— 第 13、15 章

④ 这条规则是语法还是函数?我能不能自己造一个?
它享有「不求值某参数」的特权吗?这个特权我拿得到吗(inline?宏?拿不到)?— 第 7、17 章

这四个问题,就是「求值」这两个字。把它们变成肌肉记忆,你就带走了这本书最值钱的东西。

你现在是一个不一样的工程师了

回头看第 1 章那十道题。如果你现在重做一遍,区别不在于你能答对几道 —— 而在于每一道你都能讲清楚为什么,而且你会发现它们其实是同一道题的十个侧面。

那道题是:当你写下一个表达式,机器到底做了什么?

你现在知道答案了。你知道它什么时候算、在哪个环境里算、算完去哪、这套规则能不能改。你甚至亲手写过那台做这些事的机器。

◆ 这本书真正给你的

不是 Racket。你大概率不会用它写生产代码,这从一开始就不是目标。

是一个视角的永久转变:你不再把编程语言当成一套必须背下来的戒律,而是当成一台你拆装过的机器、一组别人做的设计决定。Kotlin 为什么这样、TS 为什么那样 —— 这些不再是天条,是选择,而你现在能看懂这些选择,能评判它们,甚至能自己做。

你从语言的信徒,变成了语言的同行这就是绕道 Racket 这一趟,换回来的东西。

接下来读什么

如果这本书点着了你,这里是往下走的路。按「离这本书的距离」排:

书 / 资源它接着讲什么什么时候读
《How to Design Programs》(HtDP,免费在线)把第 9 章的「设计配方」讲成一整套方法论。Racket 的教学起点如果第 9 章是你最喜欢的一章
《Structure and Interpretation of Computer Programs》(SICP,免费)这本书卷 II、VI 的源头。元循环求值器、寄存器机、编译器 —— 深得多如果第 21–22 章让你上瘾。计算机科学的必读经典
《Crafting Interpreters》(Bob Nystrom,免费在线)把第 21–22 章那台解释器,一路升级成的字节码虚拟机。极其好读如果你想真的做一个能用的语言/DSL
《Beautiful Racket》(Matthew Butterick,在线)把第 23 章的 #lang 讲透,手把手造一堆真的小语言如果 #lang 那一章让你着迷
《The Little Schemer》用一问一答把递归思维刻进脑子。薄,奇特,经典想换种方式重练递归直觉
Racket 官方文档 + racket/contract第 24 章契约的完整体系;以及 typed/racket随时查
Haskell(以及《上游》)如果你想把「纯」「惰性」「类型」推到极致 —— 那是另一条河的上游这本书的姊妹方向
◆ 如果只读一本

读 SICP。它比这本书难、比这本书深、也比这本书老(1985)。这本《求值》某种程度上是它的一个面向 Kotlin/JS 工程师的、带互动图的导游 —— 我把它最精华的几条线(求值、环境、元循环)抽出来,对着你每天的工作讲了一遍。而 SICP 本身,是那座你现在已经站在山脚、并且知道它值得爬的山。

最后一句

这本书的名字叫《求值》,书里的机器叫 read → eval → print。

你从第 1 章的十道题(read)出发,一路把这台机器从里到外看遍、拆开、改造、重造(eval)。现在,该由你把它用出去了(print)。

回到你的 Kotlin,回到你的 JS。它们还是老样子 —— 变的是看它们的那双眼睛

读完了

回到目录 · 重做那十道题 · 返回全站