溯源:你早就在用 Haskell
每个工程师的工具箱里都躺着几件「新特性」:Kotlin 的 sealed class、Java 的 Optional 和 record、TypeScript 的判别联合、React 的纯函数组件。你大概以为它们是各家语言团队的巧思。其实打开任何一件的出厂铭牌,产地栏写的都是同一个地方。这一章我们把这些特性逐一翻过来看铭牌,再顺着水流指给你看:上游在哪,河有多长,以及为什么值得亲自去一趟。
三件你天天在用的「进口货」
先看一段最普通不过的 Kotlin。任何一个写过两年 Android 的人,都写过几乎一模一样的代码:
sealed interface UiState {
object Loading : UiState
data class Success(val items: List<Article>) : UiState
data class Failure(val error: Throwable) : UiState
}
fun render(state: UiState) = when (state) {
is UiState.Loading -> showSpinner()
is UiState.Success -> showList(state.items)
is UiState.Failure -> showError(state.error)
}
这段代码好在哪?好在状态只能是三者之一,不会出现「既在加载又有错误」的鬼状态;好在 when 少写一个分支,编译器会拍你肩膀。现在看它的上游原版——1990 年就定型的 Haskell 写法:
data UiState = Loading | Success [Article] | Failure IOException render :: UiState -> Screen render Loading = showSpinner render (Success items) = showList items render (Failure err) = showError err
不是「有点像」,是逐行对应:sealed interface 对应 data,三个子类对应竖线隔开的三个构造子,when 的分支对应三个函数子句,连「漏分支编译器报警」都是原厂功能(-Wincomplete-patterns)。Kotlin 文档不避讳这一点——sealed class 的设计参考就是函数式语言的代数数据类型(algebraic data type),我们第 4 章整章讲它。
第二件,Java 8 的 Optional 与 Stream:
Optional<User> user = repo.findById(id);
String city = user
.map(User::address)
.map(Address::city)
.orElse("未知");
这条「链式判空」的每一环,都来自 Haskell 的 Maybe 类型(1990 年代初进入标准库)。Optional.map 是 fmap,flatMap 是 >>=,orElse 是 fromMaybe。Java 的架构师 Brian Goetz 在多个场合直说过:Optional 的蓝本就是函数式语言里的 option/Maybe。第 8 章我们去看原版,你会发现原版反而更简单。
第三件,前端同学的 Redux:
// reducer: (State, Action) => State —— 一个纯函数
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'ADD_TODO': return { ...state, todos: [...state.todos, action.text] };
case 'CLEAR_ALL': return { ...state, todos: [] };
default: return state;
}
}
「状态不可变,更新是纯函数,新状态 = reducer(旧状态, 动作)」——这是 Haskell 社区玩了三十年的 fold(第 5 章)加上 Elm 架构(Elm 是 Haskell 的直系后裔,Redux 作者 Dan Abramov 明确致谢过 Elm)。React 的核心口号「UI 是状态的纯函数」,血统同样清晰。
把这一章的考据浓缩成一张表,后面每章展开一行:
| 你手里的特性 | 上游原版 | 本书章节 |
|---|---|---|
| Kotlin sealed class / Java sealed interface | 代数数据类型 data | 第 4 章 |
| Kotlin when / Java 21 switch 模式匹配 | 模式匹配(1970s ML → Haskell) | 第 4 章 |
| Optional / Kotlin ?. / TS 可选链 | Maybe | 第 8 章 |
| Result / Either / 错误码封装 | Either | 第 8 章 |
| Streams / Sequence / RxJava 惰性链 | 惰性求值 + fold/map/filter | 第 5、7 章 |
| 泛型约束 / 扩展函数的某些用法 | 类型类(1989 年论文) | 第 6 章 |
| async/await、协程的 suspend 链 | Monad 与 do 语法糖 | 第 10 章 |
| Redux / Elm 架构 / 纯函数组件 | State Monad + 纯核心架构 | 第 11、12 章 |
| jqwik / Hypothesis / fast-check | QuickCheck(2000 年) | 第 13 章 |
一条 1987 年发源的河
上世纪八十年代,惰性函数式语言在学术圈百花齐放:Miranda、Lazy ML、Orwell、Clean……每个实验室一门方言,谁也读不了谁的论文代码。1987 年 9 月,在俄勒冈波特兰的 FPCA 会议上,十几位研究者决定合并力量,设计一门统一的、惰性的、纯函数式的公共语言。委员会给它起名 Haskell,纪念逻辑学家 Haskell Curry——「柯里化」(currying)也是从他名字来的,第 2 章你就会用到。
几个年份值得记住:1990 年 Haskell 1.0 报告发布;1998 年 Haskell 98 定稿,成为此后二十年教科书的基准;2010 年更新为 Haskell 2010;此后语言的事实标准转移到了唯一的工业级编译器 GHC(Glasgow Haskell Compiler)手里,以「语言扩展」的形式继续演化,再定期打包成语言版次——GHC2021 是当前的默认版次,GHC2024 已经可以一行开启。写作本书时的稳定版是 GHC 9.12。
Haskell 社区流传一句半官方座右铭:avoid success at all costs,出自委员会成员、GHC 主要作者 Simon Peyton Jones。它常被断成「避免成功」,原意其实是断在另一处:avoid「success at all costs」——避免不惜一切代价的成功。意思是:不为了流行而在语言设计上妥协,不为兼容匆忙加进错误的特性。这个选择让 Haskell 永远成不了 Java,却让它成了三十年来最高产的特性出口国:先在上游试错十年,成熟了再由下游语言批量引进。你学它,等于直接去看下游未来十年的进货清单。
这条河今天什么样?诚实地说:生态位很清晰,但不大。金融与加密行业用它写高保证系统(Standard Chartered 的衍生品定价、Cardano 区块链),Meta 用它做反垃圾规则引擎(Sigma,每秒百万级请求),文档界的瑞士军刀 pandoc、窗口管理器 xmonad、Shell 脚本分析器 ShellCheck 都是 Haskell 写的。招聘岗位数量级远小于 Java/Kotlin——这正是本书的前提:我们不是去找工作的,是去进货的。
两种美德:纯与惰
预告一下这门语言最特别的两个性格,全书的知识都长在这两根主干上。
纯(purity):Haskell 的函数是数学意义上的函数——同样的输入永远给出同样的输出,并且不做任何别的事。不能偷偷写日志、不能改全局变量、不能发网络请求。想干这些「脏活」?可以,但必须在类型签名里亮明身份(第 11 章的 IO)。这听起来像枷锁,实际是全书最值钱的纪律:它强制你把系统劈成「纯的核心 + 脏的外壳」,而这个架构模式(functional core, imperative shell)恰恰是你能带回任何语言的头号纪念品。
惰(laziness):Haskell 默认不计算任何东西,直到有人真的需要结果。所以 [1..]——从 1 数到无穷的列表——是个完全正常的值,只要你别把它全要走。Kotlin 的 Sequence、Java 的 Stream、RxJava 的冷流,都是这个思想的手动挡版本;Haskell 是自动挡,而且深入骨髓(第 7 章)。当然,自动挡也有代价,第 16 章我们会算这笔账。
Haskell 圈的坏名声一半来自术语:Functor、Applicative、Monad,听着像要先读三年范畴论。本书的立场:这些词就是接口名,和 Comparable、Iterable 一个性质,只是起名的人是数学家。你不需要知道 Iterable 在集合论里叫什么也能遍历列表;同理,第 9、10 章我们从你写过一百遍的代码出发,最后才告诉你「你刚实现的这个东西,学名叫 Monad」。顺序是刻意的:先会用,再命名。
装上真家伙:GHCup 三分钟
本书每章都内嵌可以直接玩的浏览器 Demo,但真正的工具链值得装一套。Haskell 世界的「版本管理器」是 GHCup(类比 Android 的 sdkmanager、Node 的 nvm),一条命令拉起全家桶:
-- macOS / Linux 终端执行(Windows 有对应的 PowerShell 版): -- curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh -- -- 装好后你拥有四件套: -- ghc 编译器(写作时稳定版 9.12) -- ghci 交互式解释器,第 2 章的主战场 -- cabal 构建与包管理,第 17 章展开 -- hls Haskell Language Server,给 VS Code / IntelliJ 提供补全与类型提示
验证安装,顺便见一眼贯穿全书的提示符:
λ> 6 * 7 42 λ> putStrLn "向上游出发" 向上游出发
GHCup ≈ sdkmanager/nvm;cabal ≈ Gradle + Maven Central(仓库叫 Hackage);HLS ≈ Kotlin 语言服务;ghci ≈ Kotlin REPL,但地位高得多——Haskell 人的日常是「GHCi 驱动开发」:函数写一个,立刻扔进 REPL 里戳一戳,类型问一问(:t),比跑整套测试快一个数量级。你会在第 2 章养成这个习惯。
本书航线
你现在在河口。接下来的航程:
STATION II · 干流(第 2–7 章):语言核心。表达式与柯里化、读类型签名、ADT 与模式匹配、fold、类型类、惰性。每章都有一个「下游对照」,把新概念钉回你熟悉的语言上。
STATION III · 峡谷(第 8–12 章):效应与 Monad,劝退高发河段。我们的过法:Maybe/Either 起步(全是你写过的判空代码),爬两级台阶(Functor、Applicative),然后 Monad 自己浮出水面,最后看纯语言怎么用 IO 干脏活。
STATION IV · 上游(第 13–17 章):只有源头才有的手艺。QuickCheck 属性测试、解析器组合子、STM 事务内存、给惰性付账、以及真实项目的工程化。
STATION V · 回航(第 18–20 章):把学到的思想逐条落回 Kotlin/Java/TS/Python,附一份诚实的「什么时候别用 Haskell」清单和后续学习航线。
阅读方式:每章 15–25 分钟;代码块都值得读两遍;遇到 ▸ 泅渡点 标记的互动 Demo,请务必下水——它们不是动画,是真实现的引擎(真 Hindley-Milner 推断、真惰性求值、真 QuickCheck 收缩),你输入什么它就算什么。
1. 在你最近的项目里找一个「布尔字段堆出来的状态」(类似 isLoading + data + error 三件套),记下来——第 4 章会拿它开刀。
2. 想一想:你写过的函数里,「传同样参数、返回值却可能不同」的占几成?它们不同的原因是什么(时间?全局状态?IO?)——这是第 11 章的入场券。
3. 如果装好了 GHCup,在 ghci 里输入 :t map,把输出抄下来。看不懂没关系,第 3 章结束时你会一眼读穿它。