news 2026/8/29 1:42:31

从集合到范畴:用图解轻松理解函子、Monad 与函数式编程抽象

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从集合到范畴:用图解轻松理解函子、Monad 与函数式编程抽象

如果你在学函数式编程时,看到FunctorMonadApplicative这些名词一头雾水,那么最终的根源往往都指向同一个地方:范畴论(Category Theory)。很多资料喜欢直接抛定义,结果初学者被“对象”“态射”“自然变换”劝退。其实范畴论是一门高度图形化的学科,如果从集合论出发,一步一步看它如何被抽象出来,你会发现它并没有想象中那么难。

这篇文章围绕 “Category Theory Illustrated: From Sets to Categories” 这个主题,用图解、表格和代码示例,带你从熟悉的集合过渡到范畴。我们不讲证明,不堆定义,重点放在“为什么需要范畴论”“范畴长什么样”“它在编程中到底有什么用”这三个问题上。读完你应该能理解函子、自然变换和 Monad 的基本直觉,并能在 TypeScript 或 Python 里写出对应的最小实现。

目录:

  • 范畴论到底在解决什么问题
  • 从集合出发:构建熟悉的地基
  • 范畴的三个基本构件:对象、态射、复合
  • 用图解理解范畴:箭头与交换图
  • 经典例子:从 Set 范畴到 Monoid、Preorder
  • 函子:连接两个范畴的桥梁
  • 自然变换与 Monad:把抽象推进到实用
  • 常见理解误区与排查清单
  • 学习路线与资料建议
  • 最后说给初学者的话

1. 范畴论到底在解决什么问题

1.1 传统编程建模的底层视角是集合论

我们先回到熟悉的领域。作为一个程序员,你每天都在和“数据”打交道:

  • int是一个集合,包含所有 32 位整数;
  • string是一个集合,包含所有可能的字符串;
  • 函数f: A -> B是从集合 A 到集合 B 的一种映射;
  • 类型检查就是判断某个元素是否属于某个集合。

这种“把类型看成集合,把函数看成集合之间的映射”的视角非常直观,而且在大多数情况下够用。它来自数学里的集合论(Set Theory)。

集合论的一大利器是:它把世界拆成“元素”和“集合”。研究问题的时候,我们关心的是集合里有哪些元素,元素之间有什么运算。

1.2 集合论解释不了“结构保持”

但是当你进入函数式编程,会很快遇到一个尴尬的问题:

// 我们有一个 number[] const nums = [1, 2, 3]; // 用 map 得到 string[] const strs = nums.map(n => `数字${n}`);

这里的Array不只是一个集合,它更像是一种“结构”:它把number包成一个容器,然后你可以在容器上继续做映射。

如果严格用集合论来说,Array<number>是一个集合,Array<string>也是一个集合,map是一个函数。可是Array本身是一个“从类型到类型的操作”,它可以把任意类型T变成Array<T>。这种操作在集合论里没有直接对应的位置。

你再想一想:集合论研究的是“元素属于某个集合”,但Array更关心的是“如何保持这个容器的结构,同时把内部元素换掉”。这就不是集合论擅长的事了。

1.3 范畴论的口号:不要盯着元素,要看箭头

范畴论换了一个更“粗粒度”的视角。它不关心集合内部有哪些元素,而是关心:

  • 有哪些“对象”(Objects)
  • 对象之间有哪些“箭头”(Arrows/Morphisms)
  • 箭头如何复合(Compose)

你可以把范畴论理解为“研究箭头以及箭头之间关系的学科”。它把集合论里的“元素”隐去,只保留“对象”和“对象之间的变换”。

这个转换看起来有点反直觉,但它的威力在于:很多不同领域的结构,一旦忽略内部元素,只看对象和箭头,会呈现出高度相似的模式。你学到的抽象就可以跨领域复用。

2. 从集合出发:构建熟悉的地基

2.1 集合是什么

集合就是一堆东西放在一起。比如:

A = { 1, 2, 3 } B = { "apple", "banana" }

集合关注的是“属于”关系:1 ∈ A,表示 1 属于集合 A。

2.2 集合之间的函数

函数f: A -> B做的事情是:把 A 中的每一个元素,对应到 B 中唯一的一个元素。

def to_even(a: int) -> int: return a * 2

这里to_even就是把集合int映射到集合int

在集合论里研究函数时,我们关心的性质包括:

  • 单射(injective):不同的输入是否会产生不同的输出;
  • 满射(surjective):输出是否覆盖了目标集合的所有元素;
  • 双射(bijective):既单射又满射,两个集合可以“一一对应”。

这些性质在研究集合大小、计数问题时很有用。

2.3 集合论的局限逐渐暴露

假设现在有这样一个问题:如何描述“把任意一个集合变成以它为元素的列表”?

在集合论里,你可以定义List(A)为“以 A 中元素为元素的有限序列集合”。但你很快会发现,List不是一个普通集合,它更像一个“操作”。这种操作可以让任意集合 X 变成List(X)

进一步地,你还需要描述:

  • 如何把一个函数f: A -> B变成List(f): List(A) -> List(B)
  • 这种变换是否保持了复合关系;
  • 如果 A 和 B 之间有多条函数,List操作完这些函数后,箭头之间的组合关系会不会乱。

到这里,集合论的框架已经不好用了。因为集合论擅长描述“静态的、一层的结构”,而编程中大量存在“容器”“上下文”“结构转换”这些偏高阶的东西。范畴论就是为此而生的。

3. 范畴的三个基本构件:对象、态射、复合

3.1 对象(Objects)

范畴里的“对象”可以是你喜欢的东西。它不需要是集合,只需要作为一个“端点”存在。

比如:

  • 在 TypeScript 类型系统中,stringnumberboolean可以是对象;
  • 在数据库 schema 里,User表、Order表可以是对象;
  • 在一个有向图里,节点可以是对象。

对象不负责描述内部结构。它只是一个抽象的“place”。

3.2 态射(Morphisms)

态射是对象之间的箭头,你可以理解为“从一个对象到另一个对象的变换”。

态射和集合论中的“函数”不同:函数必须关心元素怎么映射,但态射不需要。态射只需要知道:

  • 从哪里来(source / domain);
  • 到哪里去(target / codomain);
  • 如何与其他箭头复合。

在编程里,最常见的态射就是“函数”。一个函数f: A -> B就是类型 A 到类型 B 的态射。

3.3 复合与恒等

范畴论要求每个范畴必须满足两个核心性质。

第一个性质:箭头可以复合。如果存在态射f: A -> B和态射g: B -> C,那么一定存在一个态射g ∘ f: A -> C,读作 “g 复合 f”。

这里要注意复合的顺序:g ∘ f表示先应用 f,再应用 g。很多刚接触范畴论的朋友会在这里搞混。

第二个性质:每个对象都有一个恒等态射id_A: A -> A。它和任何箭头复合后,都不会改变那个箭头:

f ∘ id_A = f id_B ∘ f = f

除了这两条,复合还必须满足结合律:

(h ∘ g) ∘ f = h ∘ (g ∘ f)

这三条规则看起来简单,但是它们是整个范畴论的地基。

3.4 一个可以立刻上手的例子:程序里的类型和函数

我们可以把编程语言中的“类型”看作对象,把“纯函数”看作态射。

  • number是一个对象;
  • string是一个对象;
  • function f(n: number): stringnumber -> string的态射;
  • function g(s: string): booleanstring -> boolean的态射;
  • 那么g ∘ f就是number -> boolean的态射。

这个“类型与函数”组成的范畴,在范畴论里非常重要。我们后面会看到,它其实就是 Set 范畴的一个编程版投影。

4. 用图解理解范畴:箭头与交换图

4.1 把范畴画成点和箭头

范畴论被称为“现代数学的通用语言”,很大程度上是因为它特别适合用图形表示。一个简单的范畴可以画成这样:

A ──f──▶ B │ │ idA idB

这张图表示:对象 A 和对象 B,以及从 A 到 B 的态射 f。

如果 f 和 g 可以复合,再画出来:

A ──f──▶ B ──g──▶ C \ ▲ └──── g∘f ───────┘

在这张图里,从 A 到 C 有两条路径:

  • 先从 A 到 B,再走 g 到 C;
  • 直接走复合箭头g∘f从 A 到 C。

如果两条路径的最终结果一致,我们就说这个图“可交换”(commute)。

4.2 交换图为什么重要

交换图是范畴论里最常用的推理工具之一。当你看到一个箭头图,并且知道它交换时,你可以把图中任意两条首尾相同的路径看成“等价”。

f A ───────▶ B │ │ h│ │k ▼ ▼ C ───────▶ D j

如果图中从 A 到 D 的两条路径k ∘ fj ∘ h相等,则这个方块交换:

k ∘ f = j ∘ h

这种“路径等价”的表达能力非常强。很多复杂的数学命题,写出来是一大串公式,画成交换图只是一张图,而且一目了然。

4.3 用代码实现箭头复合

在 TypeScript 里,我们可以用一个小小的compose函数模拟态射复合:

type Function<A, B> = (a: A) => B; function compose<A, B, C>(g: (b: B) => C, f: (a: A) => B): (a: A) => C { return (a) => g(f(a)); } // 两个普通函数 const toLength = (s: string): number => s.length; const isEven = (n: number): boolean => n % 2 === 0; // 复合出一个新函数 const isEvenLength = compose(isEven, toLength); console.log(isEvenLength("hello")); // false,长度 5 不是偶数 console.log(isEvenLength("world")); // true,长度 5 也不是偶数,这里换成 "test"

换成 Python 也一样:

from typing import Callable, TypeVar A = TypeVar("A") B = TypeVar("B") C = TypeVar("C") def compose(g: Callable[[B], C], f: Callable[[A], B]) -> Callable[[A], C]: return lambda x: g(f(x)) def to_length(s: str) -> int: return len(s) def is_even(n: int) -> bool: return n % 2 == 0 is_even_length = compose(is_even, to_length) print(is_even_length("hello")) # False print(is_even_length("test")) # True

这段代码看起来很简单,但它已经体现了范畴论的核心操作:把两个箭头复合起来,忽略中间如何实现,只关心从哪里来到哪里去。

5. 经典例子:从 Set 范畴到 Monoid、Preorder

5.1 Set 范畴:最直观的实例

范畴论里的经典起点是 Set 范畴:

  • 对象:所有集合;
  • 态射:集合之间的函数;
  • 复合:函数的复合;
  • 恒等:每个集合上的恒等函数。

这个范畴的意义在于:它把集合和函数重新组织成一个整体。当你站在 Set 范畴之上时,你不再讨论某个集合内部有哪些元素,而是讨论集合之间如何通过函数产生联系。

5.2 单元素集合与“元素”的另一种理解

用范畴论的眼光看“元素”,会有一种全新的理解方式。

考虑单元素集合{ * }。对于任何集合 X,从{ * }到 X 的一个函数,其实就是在 X 中选一个元素。

于是“元素 x ∈ X”可以被重新理解为“存在一个箭头pick_x: { * } -> X”。换句话说,范畴论不谈元素,但可以通过“从单元素集合出发的箭头”来模拟元素。

这个视角非常有用,因为它把“元素”这个概念推广了。在很多范畴里,可能没有“单元素对象”,但依然可以有类似的结构。

5.3 Monoid:一个对象与一群箭头

Monoid(幺半群)是数学中一个非常常见的结构。比如字符串拼接、列表合并、整数加法,都是 Monoid 的例子。

一个 Monoid 包含:

  • 一个集合 M;
  • 一个二元运算·
  • 一个单位元 e;
  • 满足结合律:(a · b) · c = a · (b · c)
  • 满足单位元定律:e · a = a · e = a

你可能会问:这和范畴有什么关系?

答案是:任意一个 Monoid 都可以看成一个只有一个对象的范畴。

  • 对象:一个,记作*
  • 态射:M 里的每一个元素都是一个从**的箭头;
  • 复合:就是 Monoid 的二元运算·
  • 恒等态射:就是单位元 e。

这是一个非常漂亮的对应关系。它说明范畴论比集合论更抽象,但抽象反而能揭示出结构之间深层的相似性。

5.4 Preorder:最多一条箭头的范畴

Preorder(预序)也是数学里的常见概念。一个集合上如果有一个满足自反性和传递性的关系,就构成一个 Preorder。

例如:

  • 整数上的<=关系;
  • 字符串上的“前缀”关系;
  • 容器之间的“子集”关系。

Preorder 也可以看成一个范畴:

  • 对象:集合中的元素;
  • 态射:如果a <= b,就存在一个箭头a -> b
  • 由于关系至多只有一种,所以每对对象之间最多只有一个箭头;
  • 自反性对应恒等态射;
  • 传递性对应复合。

这个例子告诉我们:范畴不一定“内容丰富”,有时候它只是在表达一种结构性的偏序。

结构对象态射要点
Set 范畴所有集合集合间的函数函数复合就是态射复合
Monoid一个对象集合元素运算变成复合
Preorder元素关系箭头关系保持自反与传递

6. 函子:连接两个范畴的桥梁

6.1 函子的定义

当我们有了两个范畴 C 和 D,就可以定义它们之间的映射,这个映射叫函子(Functor)。

一个函子F: C -> D要做两件事:

  • 对象映射:把 C 里的每个对象 X,映射为 D 里的对象 F(X);
  • 态射映射:把 C 里的每个态射 f: X -> Y,映射为 D 里的态射 F(f): F(X) -> F(Y)。

函子还必须保持结构:

  • 保证复合:F(g ∘ f) = F(g) ∘ F(f)
  • 保证恒等:F(id_X) = id_{F(X)}

简单说,函子不破坏目标范畴里的箭头关系。

6.2 编程中的 Functor:map 的本质

编程语言里的Array.map就是一个函子的经典例子。

const nums = [1, 2, 3]; const toStr = (n: number) => `数字${n}`; const strs = nums.map(toStr);

这里:

  • Array是从类型系统范畴到类型系统范畴的函子;
  • 类型number被映射成Array<number>
  • 函数toStr: number -> string被映射成Array.map(toStr): Array<number> -> Array<string>

函子定律在 Array 上体现为:

// 恒等律:map(id) === id [1, 2, 3].map(x => x); // 等价于 [1, 2, 3] // 复合律:map(f).then(map(g)) === map(g ∘ f) [1, 2, 3].map(x => x + 1).map(x => x * 2); // 等价于 [1, 2, 3].map(x => (x + 1) * 2);

6.3 自定义一个 Functor

在 TypeScript 中,我们可以用接口表示一个简单的 Functor 结构:

interface Functor<F> { map<A, B>(f: (a: A) => B, fa: F<A>): F<B>; }

虽然 TypeScript 的类型构造器支持有限,但我们可以用更直观的类来演示:

class Box<T> { constructor(private value: T) {} map<U>(f: (value: T) => U): Box<U> { return new Box(f(this.value)); } getValue(): T { return this.value; } } const box = new Box(5); const doubled = box.map(n => n * 2); console.log(doubled.getValue()); // 10

Box就是一个最简单的函子。它把内部值包起来,然后通过map在“上下文”中应用函数。

7. 自然变换与 Monad:把抽象推进到实用

7.1 自然变换:函子之间的箭头

函子连接了两个范畴。那么函子与函子之间是否也可以有箭头?答案是肯定的,这个箭头就叫自然变换(Natural Transformation)。

假设有两个函子 F 和 G,都从范畴 C 到范畴 D。一个自然变换α: F -> G对 C 中每个对象 X,给出一个 D 中的态射:

α_X : F(X) -> G(X)

并且它必须满足“自然性条件”:对任意态射 f: X -> Y,下面这个方块交换:

F(f) F(X) ──────▶ F(Y) │ │ α_X│ │α_Y ▼ ▼ G(X) ──────▶ G(Y) G(f)

自然变换的直观理解是:它能“批量地”把 F 结构转换成 G 结构,而且不依赖于具体对象。

在编程里,一个典型的例子可能是“把列表转换成数组”,或者“把 Optional 转换成列表”。只要这种转换对所有的类型都能一致地执行,它就是自然变换的雏形。

7.2 Monad:可以编程的上下文

Monad 是一个在函数式编程里被过度神化的概念。它本质上是一个自函子(从一个范畴到自身的函子)加上两个自然变换:

  • return/pure:把值放入上下文;
  • flatMap/bind/>>=:在上下文中执行函数,并把结果展平。

用类型签名来理解最容易:

interface Monad<M> { // 把普通值放入容器 pure<A>(value: A): M<A>; // 在容器内应用返回容器的函数,并展平 flatMap<A, B>(ma: M<A>, f: (a: A) => M<B>): M<B>; }

7.3 用 TypeScript 模拟 Maybe Monad

Maybe 可能是最实用的 Monad 入门案例。它用来表达“可能为空”的上下文。

type Maybe<T> = { tag: "just"; value: T } | { tag: "nothing" }; function just<T>(value: T): Maybe<T> { return { tag: "just", value }; } const nothing: Maybe<never> = { tag: "nothing" }; function pure<T>(value: T): Maybe<T> { return just(value); } function flatMap<T, U>(ma: Maybe<T>, f: (value: T) => Maybe<U>): Maybe<U> { if (ma.tag === "nothing") { return nothing; } return f(ma.value); } // 使用示例 function half(n: number): Maybe<number> { if (n % 2 === 0) { return just(n / 2); } return nothing; } const result = flatMap(flatMap(pure(10), half), half); console.log(result); // { tag: 'just', value: 2.5 } const result2 = flatMap(flatMap(pure(3), half), half); console.log(result2); // { tag: 'nothing' }

这段代码完全可以用范畴论的术语解释:

  • Maybe是一个函子;
  • pure是自然变换Id -> Maybe
  • flatMap的核心是把嵌套的Maybe<Maybe<T>>展平为Maybe<T>

Monad 真正有用的地方在于,它让你可以把“副作用”“空值”“异步”等都封装在一个上下文里,然后通过统一的方式组合。

8. 常见理解误区与排查清单

8.1 “对象就是集合”的误区

在范畴论里,对象不一定是集合。它可以是类型、表、状态、甚至一个箭头本身。初学者如果一直用集合去套范畴论,会觉得很多定义不可理喻。

正确做法:把对象当成一个“不透明的节点”。先不要追究内部结构,先看箭头关系。

8.2 “态射就是函数”的误区

态射在常见的编程例子中往往是函数,但它也可以是关系、路径、甚至推导规则。只要满足复合规则和恒等规则,就可以视为态射。

8.3 复合方向容易搞混

g ∘ f表示先 f 后 g,这个书写顺序经常让人困惑。

建议在阅读时养成习惯:凡是看到g ∘ f,先在内心翻译成 “先执行右边的 f,再执行左边的 g”。

8.4 函子定律没有验证

编程中实现 Functor 时,容易忽略函子定律。如果map不保持恒等或复合,程序可能在组合时产生诡异行为。

排查清单:

问题现象可能原因解决思路
map 之后结果和预期不一致函子实现破坏了上下文用简单的测试数据验证恒等律和复合律
无法理解自然变换混淆了函子和自然变换的层级记住:函子连接范畴,自然变换连接函子
Monad 的 flatMap 嵌套难以阅读不理解上下文含义先用 Maybe 或 Promise 这类具体类型练习
compose 顺序错了写成先 g 后 f检查代码,明确compose(g, f)表示先 f 后 g

9. 学习路线与资料建议

9.1 第一步:建立函数式编程基础

不需要直接啃范畴论教材。你可以先掌握:

  • 纯函数、不可变数据;
  • mapfilterreduce
  • 函数组合、柯里化;
  • 常用容器类型:OptionEitherList
  • 类型类或接口的概念。

这些基础是理解范畴论的“感性素材”。没有这些素材,范畴论定义读起来会很空洞。

9.2 第二步:阅读面向程序员的范畴论入门

比较推荐的通俗资料:

  • 《Category Theory for Programmers》(Bartosz Milewski)
  • 《Functional Programming in Scala》
  • 《Haskell 趣学指南》
  • 各种关于 Functor / Monad 的图解文章

建议边看边画图。把书里的定义转换成箭头图,自己尝试补全交换图。

9.3 第三步:选一门强类型语言实践

Haskell 是最适合学习范畴论的语言,因为它的类型类和范畴论术语几乎一一对应。

如果不想学 Haskell,也可以选 TypeScript、Python 或 Kotlin,自己实现:

  • Functor接口;
  • Monad接口;
  • MaybeEitherState这些常见类型。

9.4 第四步:回到经典计算机科学问题

学完基础后,你会看到范畴论在以下领域反复出现:

  • 类型系统的设计;
  • 编译优化中的变换;
  • 领域驱动设计中的聚合与领域事件;
  • 数据库查询语言的高阶抽象;
  • 并发模型中的 monad 化封装。

10. 最后说给初学者的话

范畴论确实是一门深邃的数学分支,但作为程序员,你完全可以从“图形直觉”开始,不需要先学完一门数学课再动手。

我的建议是:

  • 先画图,再定义;
  • 先写代码,再记术语;
  • 先理解map,再理解 Functor;
  • 先理解flatMap,再理解 Monad。

当你发现自己能用箭头图去描述Array.mapPromise.thenOptional.flatMap这些日常结构时,说明你已经真正跨过了“从集合到范畴”这个门槛。那个时候,再看那些看起来高深的理论定义,你会觉得它们只是给直觉补上了最后一块拼图。

如果这篇文章对你有帮助,欢迎收藏备用。下一篇文章里,我们可以继续深入函子与自然变换的代码实战。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/29 1:42:13

大脑活动量化与数据建模:Edgi 的 Strava 式活动流设计

打开电脑&#xff0c;看一眼自己的浏览器历史、笔记软件、Kindle 高亮和稍后读列表&#xff0c;混乱得像一间没有整理过的书房。但你手机上有一张很漂亮的 Strava 图表&#xff0c;记录着上周跑了多少公里&#xff1b;有一份详尽的 Letterboxd 看片清单&#xff0c;标注着每部电…

作者头像 李华
网站建设 2026/8/29 1:40:19

OT/ICS安全训练数据稀缺?从5%溯源样本看懂数据组织与异常检测

刚接手一个工控安全项目时&#xff0c;你会很自然地想找一套现成的 OT/ICS 安全训练数据集来跑通异常检测流程。但真正找过一遍的人大多会碰壁&#xff1a;公开数据要么是模拟流量&#xff0c;要么标签不完整&#xff0c;要么缺少攻击步骤的上下文。最近看到的一个方向&#xf…

作者头像 李华
网站建设 2026/8/29 1:40:14

LSM6DS3六轴传感器实战:从寄存器配置到低功耗可穿戴方案

从去年开始&#xff0c;我手上的几个可穿戴项目几乎都换上了 LSM6DS3&#xff0c;原因不复杂&#xff1a;它把 3D 加速度计和 3D 陀螺仪塞进一颗 3mm x 3mm 的封装里&#xff0c;还支持“始终开启”的低功耗工作模式&#xff0c;这让做 TWS 耳机、智能手环、姿态追踪标签这类设…

作者头像 李华
网站建设 2026/8/29 1:37:12

Turtlebot2+ROS室内自主导航系统:从SLAM建图到路径规划全解析

简介&#xff1a;自主导航是移动机器人的核心技术&#xff0c;涉及环境感知、定位与路径规划等关键环节。SLAM技术让机器人能够在不依赖外部信标的情况下构建地图并实时定位&#xff0c;而路径规划算法则确保其在动态环境中安全高效地移动。基于ROS生态&#xff0c;Turtlebot2与…

作者头像 李华
网站建设 2026/8/29 1:36:46

从“用完即弃“到“越用越懂“:Agent 记忆机制的技术拆解

Agent 的记忆不应该只是存下来。重要的加深&#xff0c;矛盾的消解&#xff0c;过时的淡忘。当前 Agent 的记忆困局 用过 ChatGPT、 Claude 或任何大模型 Agent 的人&#xff0c;大概都体验过这种挫败&#xff1a;上午你告诉 Agent&#xff0c;团队的代码规范是"所有 API…

作者头像 李华
网站建设 2026/8/29 1:36:32

西工大计算机考研上机考试真题复盘与备考心法

简介&#xff1a;在计算机考研复试环节&#xff0c;上机考试是对考生编程实践能力的直接检验&#xff0c;其本质是要求将数据结构与算法理论知识转化为可运行的代码。这种考察方式不仅验证基础功底&#xff0c;更通过在线评测系统模拟真实工程场景&#xff0c;帮助导师筛选出具…

作者头像 李华