Skip to main content

范畴论

📅 2026-04-08 ✏️ 2026-04-08 CS

1 · 范畴论

https://en.wikipedia.org/wiki/Category_theory

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#

两个函子之间的映射。对于函子 FG,自然变换把每个 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 -> BB -> A,也不意味着它们在结构上相同。因为这些映射可能会丢失信息、引入冗余,或者只能做到某种编码与解码,而不能真正恢复原状。

范畴论要求的是同构:存在 f: A -> Bg: B -> A,使得:

  • g ∘ f = id_A
  • f ∘ g = id_B

只有当两个方向的复合都等于恒等态射时,才能说明“去过去再回来”与“回来再过去”都没有任何结构损失。可逆性因此不是附加条件,而是“这两个对象其实是同一个结构”这件事的精确定义。

1.2.6 · 为什么范畴论不强调元素

集合论习惯从“对象里有什么元素”出发,而范畴论更关心“对象和其他对象之间能建立什么态射关系”。它试图捕捉的是结构如何组合、如何映射、如何被其他结构观察,而不是对象内部由哪些成分构成。

这使范畴论可以统一描述很多并不适合用“元素”来理解的对象,例如类型、群、拓扑空间、偏序集或程序语义对象。元素依赖具体实现,态射关系则更接近结构本身。

1.2.7 · 为什么 product 不需要唯一实现#

在范畴论中,product 不是通过“把两个对象拼成某个具体对象”来定义的,而是通过它在整个范畴中扮演的角色来定义的。只要某个对象 P 带有投影 π₁: P -> Aπ₂: P -> B,并满足对应的泛性质,它就是 AB 的 product。

因此,范畴论要求的不是“product 作为具体对象唯一”,而是“满足这个角色的对象唯一到同构”。如果 PP' 都满足同样的泛性质,那么它们之间必然存在唯一的同构,所以在范畴论里它们扮演的是同一个结构角色。

1.2.8 · “可替换性”和“同构”之间是什么关系

同构可以看成范畴论中“可替换性”的严格形式。若 AB 同构,那么在任何只依赖范畴结构的论证中,用 A 替换 B 都不会改变结论,因为它们在所有态射关系中表现完全一致。

因此,范畴论中常说“同构对象可以视为同一个对象”,意思并不是它们字面上相等,而是它们在结构上不可区分。所谓“同一个东西”,更准确地说,是“同构意义下的同一个东西”。

1.2.9 · 为什么必须保持 composition#

Functor 要求的不只是“把东西从一个范畴搬到另一个范畴”,而是要把可组合的结构一起搬过去。若源范畴中有 f: A -> Bg: B -> C,那么在目标范畴中必须满足:

F(g ∘ f) = F(g) ∘ F(f)

这条定律的意思是:在源世界里“先做 f 再做 g”与“把 fg 分别映过去再组合”应该是同一件事。若不保持 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 = id
  • map (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 -> BB -> 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 afmapa -> 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

详见 Functor-Applicative-Monad