ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从集合到范畴:图解范畴论核心概念与编程实践

从集合到范畴:图解范畴论核心概念与编程实践 范畴论在很多程序员眼里是“听过名字翻过两页然后放弃”的那类理论到处是抽象定义例子又少符号还不友好。这次我们看的是一个以图解为主线、从集合到范畴的入门学习项目《Category Theory Illustrated: From Sets to Categories》。它最大的特点是不急着丢给你“范畴的公理化定义”而是先让工程师从最熟悉的集合、函数、类型出发用节点和箭头把范畴论的核心概念画清楚再一步步收敛到严格定义。这个主题最值得关注的地方有三个第一它把范畴论的抽象概念还原成“画得出来”的结构降低理解门槛第二它沿着“集合 - 函数 - 箭头 - 范畴”的路径推进和程序员的类型直觉天然兼容第三范畴论中的对象、态射、复合、恒等、函子、自然变换都能在 TypeScript、Haskell、Python 里找到对应物学完可以直接动手验证。这篇文章会带你完成四件事理清范畴论的四要素和两条公理理解从集合视角切换到范畴视角的关键一步用图解方法分析一个具体范畴再通过代码示例把概念变成可运行的验证。如果你一直想搞清楚 Functor、Monad 到底在说什么这篇文章就是一个比较务实的起点。1. 核心概念速览先把范畴论里最常出现的概念和它们对应的编程直觉列出来。下面的表格贯穿全文后面所有章节都会反复引用这些名词。概念数学含义编程直觉范畴Category由对象和态射组成的数学结构一个类型系统加上所有可定义函数对象Object范畴中的“点”可以没有内部结构一个类型例如string、number态射Morphism对象之间的箭头一个函数例如string number复合Composition首尾相接的箭头组合函数组合compose(f, g)恒等态射Identity每个对象上的“不动箭头”恒等函数id x x函子Functor范畴到范畴的映射保持结构map/fmap例如Array.map自然变换Natural Transformation函子到函子的转换多态函数例如maybeToList这里需要特别强调一点范畴论里的“对象”不等于“集合”。对象在范畴中只是抽象的点它内部可以有元素也可以完全没有元素范畴论不关心对象内部长什么样只关心对象之间有哪些箭头。这一点和第 4 部分的“关键跃迁”直接相关。2. 适用场景与学习边界这个主题适合下面几类读者函数式编程学习者想从根源理解map、flatMap、compose这些操作到底在描述什么规律。类型系统研究者或对 Haskell、Rust、TypeScript 类型体操感兴趣的工程师。读过范畴论教材但觉得符号推演太多、直观例子太少的自学者。需要和数学背景同事沟通架构设计想建立一套通用抽象语言的后端工程师。它不适合以下场景想快速上手某个 Web 框架、想立刻提升业务代码运行性能的读者范畴论不会直接给你这类收益。只想要“Monad 面试题速成”的人范畴论可以解释 Monad 的来源但不能替代 API 实战。对纯数学证明不感兴趣、只想看代码效果的读者可能觉得前几章推导偏多。学习边界也要说清楚。范畴论是概念工具不是运行框架。理解范畴论能改善你的抽象能力能帮你设计更可组合的接口但它不承诺代码变快也不承诺自动写出高并发系统。它是“思维层”的工具不是“执行层”的工具。如果在学习过程中涉及代码示例建议都在本地测试环境运行不要在生产环境直接验证抽象理论代码。涉及开源库引用时注意查看许可证。3. 第一阶段从集合与函数出发范畴论图解项目通常选择“集合”作为起点是因为集合论是大多数理工科背景都能接受的直觉把一堆元素装进一个容器就是集合把每个输入对应到一个输出就是函数。在程序员看来集合和类型几乎可以划等号一种类型就是所有可能取值的集合。boolean是有两个元素的集合string是所有字符串组成的集合User是满足某个结构的对象集合。函数则对应类型签名输入一个类型输出另一个类型。// 集合类型 type User { id: number name: string } // 函数从一个集合到另一个集合的映射 const getName (user: User): string user.name这里getName接收一个User对象返回一个string本质上就是从“User 集合”到“string 集合”的映射。集合论视角下我们会关心User里有哪些字段、string的编码方式是什么但范畴论视角下这些内部细节会被暂时放到一边我们只关心一件事存在一个从User到string的箭头。所以第一阶段要建立的直觉就是类型是集合函数是集合之间的映射。这个直觉越稳固后面理解范畴就越容易。再补充一个集合与函数的关键性质函数可以复合。如果有一个函数f: A - B另一个函数g: B - C那么一定能得到一个新函数h: A - C方式是先执行f再执行g。const f (x: number): string String(x) const g (s: string): boolean s.length 3 // 复合number - boolean const h (x: number): boolean g(f(x))这一条在集合论里是显而易见的但它是整个范畴论的基石。后面定义范畴时“态射可复合”会成为核心公理之一。4. 第二阶段关键跃迁——从函数到箭头从集合到范畴最关键的跃迁不是引入更复杂的对象而是改变观察角度把注意力从“集合内部的元素”转移到“集合之间的关系”。传统集合论问的是这个集合里有哪些元素这两个集合的交集是什么元素之间存在什么运算范畴论换了一个问法这些对象之间有哪些箭头箭头和箭头能否首尾相接相接之后是否满足交换律对象内部结构反而被刻意忽略了。举一个例子。假设有两个对象A和BA是string类型B是number类型。集合论会问string里有多少字符串但范畴论只关心一件事从string到number有没有箭头有比如length、parseInt、charCodeAt这些都算数。这样一来同一个对象可以有非常多种箭头箭头本身成为研究对象。这个视角带来的直接好处是当我们讨论“从 A 到 B 的箭头”时不需要知道 A 里面到底是什么只需要知道箭头可以在复合运算下连接起来。这种抽象对工程师来说并不陌生。接口设计就是典型例子一个接口只暴露方法签名不暴露内部实现。调用方看到的不是对象内部的字段而是对象能响应哪些操作也就是“对象之间有什么箭头”。从集合到范畴就是把这种工程直觉拔高成数学结构。再进一步理解函数是特殊的箭头但箭头不只是函数。在某个具体范畴里箭头可以是关系、逻辑蕴含、程序执行路径、数据库外键关系只要它们满足复合和恒等规则就能构成范畴。这也是范畴论抽象能力强的原因它抓住的是“关系与复合”这一共同骨架。5. 第三阶段范畴的四要素与两条公理当你接受“把函数当成箭头”之后范畴的正式定义就顺理成章了。一个范畴由以下四个要素组成。5.1 对象对象是范畴中的基本节点。在集合范畴Set中对象就是集合在类型范畴中对象就是类型。对象内部结构不重要重要的是它作为箭头的起点和终点。5.2 态射态射是对象之间的箭头。每个态射都有一个源对象和一个目标对象。如果态射f的源对象是A、目标对象是B写作f: A - B。在Set范畴里态射就是集合之间的函数。5.3 恒等态射每个对象都必须有一个恒等态射从对象指向自身并且和其他箭头复合时不起作用。在集合范畴中恒等态射就是恒等函数id输入什么就返回什么。5.4 复合运算态射必须支持复合。如果f: A - Bg: B - C那么存在一个态射g ∘ f: A - C。复合运算需要满足两条公理。结合律h ∘ (g ∘ f) (h ∘ g) ∘ f只要箭头首尾能接上复合顺序不影响结果。单位律对任意f: A - B都有f ∘ idA f且idB ∘ f f。这两条公理不要求对象有任何内部结构也不要求态射一定是函数。它们描述的是箭头之间最基础的组合规则。用 TypeScript 可以直接把这些定义变成代码// 态射从一个类型到另一个类型的函数 type MorphismA, B (input: A) B // 恒等态射每个对象 A 都有一个 id const id A(x: A): A x // 复合运算先执行 g再执行 f const compose A, B, C( f: MorphismB, C, g: MorphismA, B ): MorphismA, C (x: A): C f(g(x)) // 验证结合律compose(h, compose(g, f)) compose(compose(h, g), f) // 验证单位律compose(id, f) f这段代码基本就是集合范畴在类型系统里的缩影。对象对应类型态射对应函数恒等态射对应id复合对应compose。如果读者能把这段代码跑通再回头看书上的抽象定义会轻松很多。6. 图解形式把范畴画出来项目标题里的Illustrated提示我们图解是这个主题的贯穿线索。范畴论的抽象概念一旦画出来叙述会清晰很多。6.1 节点与箭头最基础的图就是节点加箭头。每个节点代表一个对象每个箭头代表一个态射。比如两个对象A和B存在态射f: A - B就画成A ── f ──▶ B看起来很简单但很多推理都可以在图上完成。对象和态射一旦用图表示复合就变成了“两条首尾相接的箭头可以合并成一条新箭头”。6.2 复合的图如果f: A - Bg: B - C那么图上有两条路径可以从A到C直接走g ∘ f或者先走f再走g。范畴论要求这两条路径代表同一个态射这就是复合的定义A ── f ──▶ B │ │ │ g │ ▼ └── g∘f ──▶ C一旦开始用路径思考范畴论就从“符号游戏”变成了“箭头拼图”。6.3 交换图更复杂的范畴关系用交换图表示。比如下图A ── f ──▶ B │ │ g h ▼ ▼ C ── i ──▶ D如果从A到D的两条路径相等即h ∘ f i ∘ g这个图就叫交换图。交换图的特点是同一起点到同一终点的不同路径结果一致。交换图在范畴论中的地位很高因为很多定义就是靠“某个图可交换”来描述的。函子保持复合结构、自然变换满足自然性条件本质上都是在说某些图可以交换。图解学习的好处是每个抽象定义都能转成几何判断不懂某个等式时先画出所有箭头和路径看哪些路径是等价的图形结构可以嵌套一个复杂交换图可以被拆成多个小方块分别验证。7. 进阶函子与自然变换理解了范畴本身下一步就是看范畴之间的映射这就是函子Functor。函子可以理解成“保持结构的映射”它不仅把范畴C里的每个对象映射到范畴D里的某个对象还把C里的每个态射映射成D里的某个态射并且必须保留复合和恒等关系。程序员最熟悉的函子就是各种容器类型。Array、Promise、Maybe都是一等公民的函子。它们允许你在保持包装结构的前提下把某个函数应用到内部值上。Haskell 中的定义最有代表性class Functor f where fmap :: (a - b) - f a - f bfmap接收一个普通函数a - b返回一个在函子上下文中运行的函数f a - f b。对应到 TypeScriptconst arrayMap A, B(f: (x: A) B, arr: A[]): B[] arr.map(f) const maybeMap A, B(f: (x: A) B, m: A | null): B | null m null ? null : f(m)arrayMap和maybeMap的共同点是它们都把一个普通函数提升到某个上下文里执行且不破坏上下文的形状。这正是函子的核心。自然变换Natural Transformation则更进一步它是函子之间的转换。一个自然变换能把一个函子的结果转换成另一个函子的结果同时不依赖具体对象。函数式编程中常见的maybeToList就是自然变换把Maybe a转成[a]而且这种转换对所有类型a都是一致的不会针对某个具体类型特殊处理。def maybe_to_list(maybe): return [maybe] if maybe is not None else [] print(maybe_to_list(42)) # [42] print(maybe_to_list(None)) # []Python 代码不够类型化但能很直观地展示“上下文转换”的含义不改变内部值的语义只改变包装结构。这个例子在 Haskell 里写作maybeToList :: Maybe a - [a] maybeToList Nothing [] maybeToList (Just x) [x]函子和自然变换是范畴论中两个高频出现的概念也是理解 Monad、Lens、Profunctor 的前置知识。对多数工程师而言先理解 Functor 就够了自然变换可以作为进阶方向。8. 学习过程中的常见误解与排查学范畴论最容易栽跟头的地方不是计算而是概念层面的误判。下面整理了几个高频误解和排查方向。常见误解正解排查方向把范畴论里的对象当成集合对象是抽象节点可以有内部结构也可以没有当你试图“查看对象内部”时停下来改看箭头把所有态射都当成函数函数是态射的重要实例但态射还可以是逻辑蕴含、数据库关系等问自己这里是否只依赖源、目标、复合是的话就是态射忽略恒等态射每个对象必须有恒等态射这是范畴公理的一部分画图时检查每个节点是否有自环箭头混淆函子和普通函数函子是范畴之间的映射而不是单个集合之间的映射看到fmap时注意f a这层包装不能丢失认为交换图自动成立交换图是等式约束需要逐个路径验证从同一起点分别走不同路径对比终点是否一致不理解“抽象”的意义抽象是为了让不同领域的结构复用同一种推理方式试着在关系数据库里找一组“对象 外键 路径”的对应关系排查的具体做法也很简单当一段范畴论推导看不懂时先用纸笔把箭头画出来如果图已经画出来了还是看不懂就找一个具体范畴实例比如Set范畴或者 TypeScript 类型范畴把抽象箭替换成具体函数再用代码跑一遍。多数卡住的情况都是因为缺少一个“具体落点”。9. 最佳实践与后续学习路线学习范畴论不需要一上来就啃大部头教材更推荐按下面的路径推进。第一先建立“对象 箭头”的视觉直觉不要急着记定义。遇到一个概念先画图再写定义。画图的时候至少包含两个对象、两个箭头和一个复合箭头。第二每种定义都写至少一段代码。用 TypeScript 写状态映射和复合用 Haskell 写类型类用 Python 写可能函数。代码能把抽象概念固定到具体语法上避免“看书看懂了动手就忘”。第三从Set范畴入手再扩展到列表范畴、Maybe范畴。Set范畴的优点是直观但习惯之后要提醒自己范畴不限于集合。可以尝试把编程里的状态迁移看成一个范畴状态是对象状态迁移是态射连续迁移是复合。第四每学习一个新概念都检查它是否保持了“复合”和“恒等”。函子必须满足fmap id id、fmap (g . f) fmap g . fmap f。用一个二维数组或Maybe实测这两个等式概念会牢固很多。第五控制学习节奏。范畴论是一个可以无限深入的分支第一次接触不要试图一天理解 Yoneda 引理。先掌握 Functor再看 Applicative之后才是 Monad。顺序反过来会非常痛苦。后续可以继续了解的方向包括Monad 及其在 IO、异步编程中的应用Lens 与组合 lens 的数学基础Profunctor 与双向映射以及 Yoneda 引理与其在序列化、缓存设计中的直观解释。每个方向都能以本文的“集合到范畴”为基础往上叠加。10. 总结这个主题最值得尝试的地方是把范畴论从“黑板上的符号”变成“可以画出来、可以用代码运行”的结构。从集合到范畴本质上只做了一次视角切换把注意力从元素转到箭头把函数抽象成可复合的关系。建议你最先验证的概念是复合与恒等。用 TypeScript 写一个compose跑通单位律和结合律再去看 Functor 的fmap会发现后面的一切都是在这个基础上加包装层。最容易踩的坑就是“把对象当成集合”。带上这个执念读后面的内容会一直觉得范畴论在绕弯子一旦接受对象只是箭头的端点很多符号定义就通顺了。下一步可以继续沿着“函子 - 自然变换 - Monad”的方向走结合你正在使用的语言找到对应的map、flatMap、compose实现逐个验证它们是否满足范畴律。这套从集合到范畴的图解思路可以作为长期使用的抽象思维脚手架。
返回列表