STATION I · 河口CH 01里程 01/20

溯源:你早就在用 Haskell

每个工程师的工具箱里都躺着几件「新特性」:Kotlin 的 sealed class、Java 的 Optional 和 record、TypeScript 的判别联合、React 的纯函数组件。你大概以为它们是各家语言团队的巧思。其实打开任何一件的出厂铭牌,产地栏写的都是同一个地方。这一章我们把这些特性逐一翻过来看铭牌,再顺着水流指给你看:上游在哪,河有多长,以及为什么值得亲自去一趟。

特性溯源语言史GHCup航线图

三件你天天在用的「进口货」

先看一段最普通不过的 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.mapfmap,flatMap>>=,orElsefromMaybe。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-checkQuickCheck(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,听着像要先读三年范畴论。本书的立场:这些词就是接口名,和 ComparableIterable 一个性质,只是起名的人是数学家。你不需要知道 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 章结束时你会一眼读穿它。