范畴论
Backlinks (0)
No backlinks found
1 · 范畴论
S 软件工程中,我们不断地在做”组合”——函数组合、类型组合、数据变换的管道化。我们希望能对这些组合行为进行精确、通用的推理。 C 不同领域(类型系统、函数式编程、数据流管道、设计模式)各自独立地发明了相似的组合模式(如 map/flatMap、Functor、Monad),但缺乏统一的理论框架,导致无法跨领域迁移洞见,也难以判断某个抽象是否”正确”或”完备”。 Q 是否存在一种数学框架,能抓住”组合”与”结构保持变换”的本质,从而统一这些模式? A 范畴论正是”组合的数学”。它只关心对象(object)之间的态射(morphism/arrow)以及态射的组合规则,而不关心对象的内部结构。通过 Functor、Natural Transformation、Monad 等概念,它为类型系统设计、程序语义、数据变换管道等提供了一套严格且可迁移的抽象语言。
1.1 · 核心概念
1.1.1 · 范畴 Category#
一个范畴由三部分组成:
- 对象(object):事物
- 态射(morphism/arrow):对象之间的映射
- 组合规则:态射可组合,满足结合律,且每个对象有恒等态射
1.1.2 · 函子 Functor#
范畴之间的结构保持映射——把对象映射到对象,态射映射到态射,且保持组合和恒等。
若映射的目标范畴和源范畴相同,则称为自函子(endofunctor)。
1.1.3 · 自然变换 Natural Transformation#
两个函子之间的映射。对于函子 F 和 G,自然变换把每个 F(a) 转换为 G(a),且与函子的态射映射可交换。
1.1.4 · 单子 Monad#
“A monad is just a monoid in the category of endofunctors.” 单子不过是自函子范畴上的幺半群。
Monad = 自函子 F + 两个自然变换,满足幺半群法则:
- 运算(
μ):F(F(a)) → F(a)(两层压成一层 = 两个自函子”合并”) - 单位元(
η):a → F(a)(套一层最平凡的结构 = 空操作) - 满足结合律和单位律
1.2 · 直觉与关键问题
1.2.1 · 为什么函数天然适合当态射
函数天然满足范畴对态射的两条要求:
- 函数组合满足结合律:
(f ∘ g) ∘ h = f ∘ (g ∘ h) - 每个类型都有恒等函数
id :: a -> a
也就是说,在“类型作为对象、函数作为态射”的视角下,不需要额外补充什么结构,就已经得到一个范畴。范畴论在这里不是凭空发明了一套规则,而是把函数组合中早已存在的规律抽象了出来。
1.2.2 · 为什么“能组合”比“对象是什么”更重要
范畴论强调的不是对象的内部构造,而是对象之间是否能通过态射连接,以及这些态射如何组合。对 f ∘ g 而言,我们并不需要知道中间对象的内部细节,只需要 g 的输出能接上 f 的输入。
因此,一个对象的“本质”不是靠拆开内部结构来定义,而是靠它与其他对象之间的态射关系来刻画。这正是 Yoneda 引理背后的核心直觉:要理解一个对象,关键不是看它“里面是什么”,而是看它在所有可能的映射关系中如何表现。
1.2.3 · 为什么 monoid 可以看成 category#
一个幺半群 (S, ·, e) 可以完全等价地看成一个只有一个对象的范畴:
- 唯一对象记作
* S中每个元素对应一个态射* -> *- 态射的组合就是幺半群运算
· - 恒等态射就是单位元
e
幺半群的结合律与单位律,恰好就是范畴对态射组合的公理。因此,monoid 不是“类似于” category,而是 category 的一个特例:只有一个对象的范畴。
1.2.4 · 为什么 identity 必须存在#
恒等态射的作用不是提供额外行为,而是提供“什么都不做”这个基准。没有 identity,组合系统会缺少一个最基本的参照点:
- 无法表达“这个操作前后不发生变化”
- 无法把对象稳定地纳入组合链条中
- 很多结构保持定律将无法表述,例如 functor 必须保持恒等:
fmap id = id
这和幺半群中的单位元是同一个道理。单位元看起来像“没有作用”的元素,但正是这个“无作用”保证了整个组合结构的完整性。
1.2.5 · 为什么“可逆”比“能互相转”更关键
两个对象之间即使都存在映射 A -> B 和 B -> A,也不意味着它们在结构上相同。因为这些映射可能会丢失信息、引入冗余,或者只能做到某种编码与解码,而不能真正恢复原状。
范畴论要求的是同构:存在 f: A -> B 与 g: B -> A,使得:
g ∘ f = id_Af ∘ g = id_B
只有当两个方向的复合都等于恒等态射时,才能说明“去过去再回来”与“回来再过去”都没有任何结构损失。可逆性因此不是附加条件,而是“这两个对象其实是同一个结构”这件事的精确定义。
1.2.6 · 为什么范畴论不强调元素
集合论习惯从“对象里有什么元素”出发,而范畴论更关心“对象和其他对象之间能建立什么态射关系”。它试图捕捉的是结构如何组合、如何映射、如何被其他结构观察,而不是对象内部由哪些成分构成。
这使范畴论可以统一描述很多并不适合用“元素”来理解的对象,例如类型、群、拓扑空间、偏序集或程序语义对象。元素依赖具体实现,态射关系则更接近结构本身。
1.2.7 · 为什么 product 不需要唯一实现#
在范畴论中,product 不是通过“把两个对象拼成某个具体对象”来定义的,而是通过它在整个范畴中扮演的角色来定义的。只要某个对象 P 带有投影 π₁: P -> A 和 π₂: P -> B,并满足对应的泛性质,它就是 A 与 B 的 product。
因此,范畴论要求的不是“product 作为具体对象唯一”,而是“满足这个角色的对象唯一到同构”。如果 P 和 P' 都满足同样的泛性质,那么它们之间必然存在唯一的同构,所以在范畴论里它们扮演的是同一个结构角色。
1.2.8 · “可替换性”和“同构”之间是什么关系
同构可以看成范畴论中“可替换性”的严格形式。若 A 与 B 同构,那么在任何只依赖范畴结构的论证中,用 A 替换 B 都不会改变结论,因为它们在所有态射关系中表现完全一致。
因此,范畴论中常说“同构对象可以视为同一个对象”,意思并不是它们字面上相等,而是它们在结构上不可区分。所谓“同一个东西”,更准确地说,是“同构意义下的同一个东西”。
1.2.9 · 为什么必须保持 composition#
Functor 要求的不只是“把东西从一个范畴搬到另一个范畴”,而是要把可组合的结构一起搬过去。若源范畴中有 f: A -> B 与 g: B -> C,那么在目标范畴中必须满足:
F(g ∘ f) = F(g) ∘ F(f)
这条定律的意思是:在源世界里“先做 f 再做 g”与“把 f、g 分别映过去再组合”应该是同一件事。若不保持 composition,那么组合链条在映射后就会断裂,模块化推理、管道化推理和重构等价性都会失效。那样的映射就不是“结构保持”,而只是某种松散的改写。
map 为什么是 Functor 的体现#
Functor 的核心不是“改值”,而是“把一个态射提升到某个上下文中”。若有函数 f: a -> b,Functor 要把它映为:
F(f): F(a) -> F(b)
在 Haskell 里,这个动作就是 fmap。对 List 这个 functor 而言,fmap 的具体实现名正是 map:
a被映为[a]f: a -> b被映为map f: [a] -> [b]
所以 map 的本质不是“遍历容器”,而是“把普通函数的组合结构搬到容器上下文里”。也因此它必须满足:
map id = idmap (g . f) = map g . map f
这正是 functor 保持恒等与组合的体现。
1.2.11 · 为什么 Functor 不是“随便一个转换函数”#
普通函数只需要把一个值变成另一个值,例如 length :: [a] -> Int。但 Functor 不是单个值级别的变换,而是一种对象和态射同时发生、且彼此一致的映射规则。
要成为 functor,至少要满足三点:
- 对每个对象给出映射
- 对每个态射给出映射
- 保持恒等与组合
因此,Functor 不是“随便找个函数包一层”就行,而是要求整个变换系统在结构上前后一致。若一个映射不能说明“每个箭头如何被系统性地搬运”,或者搬运后不再保持组合,那它就不是 functor。
1.2.12 · 如果一个系统不能保持组合,还能叫系统吗
广义上当然仍然可以称作一个系统,但它很难称为一个可组合、可推理的结构系统。一个不能稳定保持组合的系统,通常会出现这些问题:
- 局部操作各自成立,但接起来后语义漂移
- 模块之间难以通过统一接口组合
- 小范围正确性无法推广到整体正确性
- 重构会破坏原本的行为等价关系
范畴论关心的不是“某些操作能不能偶尔接上”,而是“组合是否本身构成一个稳定规律”。若这一点做不到,就很难形成可复用的抽象,也很难把这个系统提升为可普遍推理的数学对象。
1.2.13 · 为什么“先构造再用”能等价于“直接用”
这类现象通常由泛性质刻画;更系统地说,常常来自一对伴随(adjunction)。直觉上,F 负责“自由地构造出某种结构”,G 负责“忘掉结构,只看底层对象”,二者满足:
Hom_D(F(a), b) ≅ Hom_C(a, G(b))
它的意思不是说 F(a) 和 b 是同一个对象,而是说:
- “先把
a构造成F(a),再在结构世界里使用它” - “直接在原世界里说明
a该如何指向G(b)”
这两种做法携带的是同一份信息。原因在于,F(a) 不是随便造出来的对象,而是“恰好够表达所有合法组合”的自由对象;从它出发的结构保持映射,完全由 a 中那些最原始的数据如何送入目标对象所决定。
1.2.14 · 为什么这不是简单的 encode/decode#
encode/decode 通常描述的是对象级别的往返变换,例如 A -> B 与 B -> A。但这里说的不是“两个对象彼此可逆”,而是两类映射空间之间存在自然对应:
- 一边是
F(a) -> b这样的结构保持映射 - 另一边是
a -> G(b)这样的普通映射
所以这不是把某个对象编码后再解回来,而是在说:你在“自由构造后的结构世界”里做的事,与在“原始对象世界”里直接指定生成元如何使用,本质上是一回事。它比较的是“如何使用对象”,不是“对象本身长得像不像”。
1.2.15 · 为什么说它是“最优关系”
这里的“最优”不是数值意义上的最小或最大,而是说这对关系刚好把“加结构”和“忘结构”严丝合缝地配平了:
F加的是最少但足够的结构G去掉的是结构外壳,保留底层可观察内容- 任意一个普通映射
a -> G(b),都唯一对应到一个结构保持映射F(a) -> b
因此,自由构造既没有多加妨碍使用的限制,也没有少到不足以承载组合规则。它给出的不是“某种可行翻译”,而是“把原始对象嵌入结构世界的最佳方式”。
1.2.16 · 为什么 Monad 会从这里出来#
一旦有伴随 F ⊣ G,就会自动得到一个自函子:
T = G ∘ F
也就是“先自由构造,再忘回原世界”。由于 T 仍然落在原范畴里,它可以反复作用在对象上,于是自然出现两件事:
η: a -> T(a):把普通对象放进这层“自由结构”中μ: T(T(a)) -> T(a):把“先构造、再构造”压平成“一次构造”
这正是 Monad 的单位与乘法。换句话说,Monad 不是凭空定义出来的神秘接口,而是“自由构造这件事可以连续做很多次,并且这些连续构造可以一致地压平”这一事实的抽象。
以 List 为例,可以把它理解成“自由 monoid”:
η对应x -> [x]μ对应join :: [[a]] -> [a]
所以 Monad 会从这里出现,是因为“先加结构再回来”的过程本身已经构成了一个可组合系统,而这种可组合性正好满足 Monad 的法则。
1.3 · 在 Haskell 中的对应#
Haskell 工作在 Hask 范畴中:对象是类型,态射是函数 a -> b。
| 范畴论 | Haskell |
|---|---|
| 自函子 | Functor typeclass,如 Maybe 把类型 a 映射到 Maybe a,fmap 把 a -> b 映射到 Maybe a -> Maybe b |
| 自然变换 | 多态函数,如 maybeToList :: Maybe a -> [a](Maybe 函子到 List 函子的变换) |
Monad 的 μ(合并) | join :: F (F a) -> F a |
Monad 的 η(单位元) | return :: a -> F a |
μ 和函子映射的组合 | >>= :: F a -> (a -> F b) -> F b(即 fmap + join) |