回流:带回 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 / reduce、sumOf、groupBy | fold 是列表递归的模式 | 10 |
| 判别联合、「让非法状态无法表示」、Redux action | 代数数据类型 + 模式匹配 | 11 |
Sequence、function*、Stream、Flow | 惰性求值;正规序的手动模拟 | 12 |
回调地狱、throw、return、await | 续延:「接下来要做的事」 | 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、配置里的逻辑 | 元循环求值器:一个小 eval | 21–22 |
十二件能立刻用的东西
对照表是索引,这一节是行动项。不是「理解了」,是「下周就改」:
1. 反过来用 REPL。先在 Kotlin Scratch File / Node REPL 里把逻辑跑通,再搬进项目。你按「编译运行」的次数会掉一半。(第 2 章)
2. 用你语言的「Macro Stepper」。Kotlin 的 Show Bytecode → Decompile、TS Playground 的输出栏 —— 看编译器真的把你的 data class / async 变成了什么。用它的人和不用的人不在一个层次。(第 2 章)
3. 卡壳时,关掉编辑器,先写数据的形状。「这个函数怎么写」难,常常是因为「我在处理什么形状的数据」还没想清。先答:有几种情况?每种有什么字段?哪个字段是递归的?(第 9 章)
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. 该延迟求值的参数,别设计成普通参数。问一句「调用者是否可能希望它不被求值?」是 → 用 lambda 参数(require(c){...}、getOrElse{...})。Optional.orElse 的坑就是没这么做。(第 7 章)
10. require 与 check 分清 —— 它们是 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。它们还是老样子 —— 变的是看它们的那双眼睛。
(((((( 读完了 ))))))