
This process of hierarchical decomposition and recomposition is not imposed on us by computers. It reflects the limitations of the human mind.
— Bartosz Milewski, Category Theory for Programmers: The Preface
序
工程学的核心是管理复杂性
对于如何分解
- 出没在各种函数式编程社区
通篇充斥着抽象废话 (abstract nonsense), - 热衷于展示一些难懂的交换图
但看起来只是装模做样, - 喜欢嘲讽任何非纯的程序语言
尤其是取笑这些语言的锁机制, - 标榜自己的代码只在主逻辑传入参数
定义函数根本不需要形参, - 转身打开编辑器偷偷写一些
do ...
return ...
这样看上去和其他语言别无二致的东西 - 被吃瓜群众问到以后
涨红脸说些, 非纯……Monad 的事“ 能叫非纯吗……, 这样难懂的话” - 被要求解释一下什么是 Monad
叽里咕噜半天, 什么打开盒子装进盒子, 一些具体的抽象废话, - 现实中
据说这些人真的参与大型软件项目, 但谁也没在身边见过, 打听来打听去, 好像这些人只能混迹在银行和铁路这些老古董行业, 什么, 编译器? 那也是老古董?

我本人并非 Haskell 程序员
起初
后来我遇到了一些困难
然而map
和 filter
的简单用法后
这份讲义是我们为后来者绘制的地图
对读者的期待
严格讲
我们期待读者对一切充满好奇

不过
计算机程序的构造与解释《 》 大名鼎鼎的 SICP: 如果将函数式编程看作一门真正的外语[4], 比如英语, 那 SICP 就相当于, 新概念英语《 》 你后来意识到它有这样那样的局限和缺憾。 但当你回忆对这门外语最初的感觉, 那些模糊的轮廓和形貌, 无一例外都源于它, 你不会后悔读过它。 因为是它教会你如何用这门外语思考和表达, 切换到技术性的视角。 在前两章: 书中将, map
应用在多种数据结构上 引导读者思考背后的共性, 暗示了函子 (functor) 的存在, 类似还有; accumulate
暗示了幺半群对象 (monoid object)——只是 SICP 并未明文点出这些概念 同时。 第二章的许多概念为 Haskell 提供了雏形, 例如2.2 层次性数据和闭包性质, 这一节与代数数据类型 (ADT) 有着直接关联, 还有2.5 带有通用性操作的系统; 我们熟悉的类型类, type class( 正是在这个想法启发下诞生的) 在第三章。 读者会直面现实的混沌, 认识纯函数和非纯函数, 并养成自己的品味——你愿意为了一方面的优势而容忍另一方面的代价吗, 作为整本书最深刻的? 巧合“ ” 在3.5 流这一节中, SICP 事实上在流计算中完整构造了一个, Monad
——我们的讲义将会解释为何会出现这种巧合 当然。 SICP 还包含无数我们没有精力提及的概念, 例如惰性求值, 抽象屏障等等、 它们同样重要, 我们鼓励读者花点时间弄明白这些概念, 最后——我是说真正的最后。 在读者, 还有我, 完成这份讲义后, 演出谢幕之时——我们可以回去看看2.1.3 数据意味着什么, 在范畴论的, 高观点“ 下” 我们能回答这个问题吗, ? Haskell 趣学指南《 》 这本书可以帮助读者了解 Haskell 的基本语法: 强化函数是一等公民的概念, 通过, map
和fold
巩固函数式编程的初步直觉 通过二叉树的例子接触持久化数据结构——这是纯函数式语言最地道 (idiomatic) 的数据结构, 读者会接受充分的训练。 学会识别 Haskell 对象的类型签名, 然后越来越熟练, 越来越快, 我们特别推荐第十二章的走钢索。 这是, Monad
最好的入门实例 如果读者在任何时候觉得自己迷失在, Monad
的森林里 都可以折回来再看一眼这个例子, 况且。 扮演一个走钢索的杂技演员本身也很有趣, 。
我们期待读者接受过基本的数学训练——别担心
我们期待读者在旅途中保持审慎
最后
符号与样式
数学部分
首先约定一下数学概念的符号和样式
- 范畴 (category) 使用英文草书体大写字母
\mathcal
例如一般的范畴 $\mathcal{C},\ \mathcal{D}$, 还有 Hask 范畴 $\mathcal{H}$, - 对象 (object) 使用英文意大利体大写字母
即数学环境的默认字体, 例如 $A,\ B,\ X$, - 态射 (morphism) 使用英文意大利体小写字母
例如 $f,\ g$, 特别地; 用 $1_{X}$ 表示对象 $X$ 的单位态射, 态射复合记为 $g \circ f$; 用 $\mathrm{Hom}_{\mathcal{C}}(A,\ B)$ 表示; 范畴 $\mathcal{C}$ 上从对象 $A$ 到对象 $B$ 的所有态射组成的集合“ ” 上下文清楚时——几乎是所有时候——我们省略下标, 直接记为 $\mathrm{Hom}(-,\ -)$, - 函子 (functor) 同样使用小写字母
但使用德文尖角体, \mathfrak
以便与态射区分, 例如 $\mathfrak{f},\ \mathfrak{g}$, 用 $\mathfrak{f}(A)$ 和 $\mathfrak{f}(h)$ 分别表示函子 $\mathfrak{f}$ 的对象映射和态射映射; - 自然变换 (natural transformation) 使用小写希腊字母
例如 $\alpha,\ \eta$, 如要强调该变换在某对象 $X$ 上的分量 (component); 用对象名作为下标, 即 $\alpha_{X}$, - 在自函子范畴 (endofunctor category) 上
沿用德文尖角体表示自函子, 特别地; 恒等函子记为 $\mathfrak{id}$, 函子 $\mathfrak{f}$ 到自身的恒等自然变换记为 $\mathrm{id}_{\mathfrak{f}}$, 用 $\beta \circ \alpha$ 表示两自然变换的竖直复合; 这与态射复合的记号保持一致, 用 $\gamma * \eta$ 表示两自然变换的水平复合; 此外; 用 $\mathrm{Endo}(\mathcal{H})$ 表示 Hask 范畴的自函子范畴, 在提及恒等函子对某对象 $X$ 的作用时; 我们习惯直接用 $X$ 代替 $\mathfrak{id}(X)$, - 积 (product) 的泛性质 (general property) 用三元组 $\{A \times B,\ p_{1},\ p_{2}\}$ 表示
另一方面; 余积 (coproduct) 的泛性质用三元组 $\{A+B,\ i_{1},\ i_{2}\}$ 表示, 用 $h=\left<f,\ g\right>$ 表示积和余积的态射运算; - 用德文尖角体大写字母表示图示 (diagram)
多数时候我们不会直接给图示命名, 而是直接说, 以下几张图交换“ ” 如果我们需要特别提及——譬如在定义极限和余极限时——某张图示; 记为 $\mathfrak{F},\ \mathfrak{G}$,
Haskell 部分
在 Haskell 语言中存在大量与范畴论对应的概念
- 原则
所有文档中的 Haskell 概念都使用等宽字体: \texttt
或\mathtt
- 特例
对于Hask 范畴和Hask 范畴上的自函子范畴这两个特殊概念: Haskell 语言的代码中不会显式提及它们, 因此我们仍然沿用数学概念的记号 $\mathcal{H}$ 和 $\mathrm{Endo}(\mathcal{H})$, - 范畴论中的对象
对应 Haskell 中的类型 (type), 用 $\mathtt{a,\ b,\ c,\ a1,\ a2}$ 等表示任意类型, 即 Hask 范畴 $\mathcal{H}$ 中的任意对象, 极少数时候; 我们会提及具体的类型, 例如 $\mathtt{Bool,\ Int}$ 等, - 态射对应 Haskell 中的函数 (function)
用 $\mathtt{f,\ g}$ 等作为函数名, 指代某个具体的态射, 而函数类型签名体现了这个态射的源对象 (domain) 和目标对象 (codomain), 例如, : $$\mathtt{f\ ::\ a\ \to\ b}$$这个函数签名表示 名为“ f
的函数接受类型为a
的输入同时输出类型为b
” 对应到范畴论的概念上, 即, 态射 $f$ 将源对象 $A$ 指向目标对象 $B$“ ” - 函子对应 Haskell 中的类型构造器 (type constructor)
例如, Maybe
和[]
必须特别注意; 我们也会用 $\mathtt{f,\ g}$ 表示任意函子, 我们尽力避免函子与态射的记号相互混淆, 但我们无奈地承认这种可能性, 读者必须根据上下文自行甄别——尤其是出现对象映射时, 读者必须提高警惕, 函子的态射映射 $\mathfrak{f}(h)$ 在 Haskell 中对应; fmap h :: f a -> f b
其中态射 $h$ 的类型签名为, h :: a -> b
- 自然变换在 Haskell 上非常有趣
我们通过一个参数多态 (polymorphism) 的 Haskell 函数——这里随意命名为, eta
——来表示函子f
到函子g
的自然变换: $$\mathtt{eta\ ::\ forall\ a.\ f\ a\ \to\ g\ a}$$用范畴论的视角看 参数多态的 Haskell 函数其实并非一个单独的态射, 而是以任意对象 $\mathtt{a}$ 为参数的一族态射 $\eta$, 取参数 $\mathtt{a} = A$, 得到其在对象 $A$ 上的分量为 $\eta_{A}$ ——这恰好是自然变换的定义, ! - 积在 Hask 范畴上对应元组 (tuple)
(,)
即 $\{\mathtt{(a,\ b),\ fst,\ snd}\}$, 对应的态射运算为, f &&& g
而余积则对应; Either
即 $\{\mathtt{Either\ a\ b,\ Left,\ Right}\}$, 对应的态射运算为, f ||| g
或either f g
体例
这份讲义的目标是建立以下三者的联系
- 范畴论
- Haskell 编程语言
- 现实中可以被抽象出来的典型计算过程和数据关联
瞄准目标
强调数学家视角和程序员视角的差异与联系
对于保持了源范畴之幺半结构的函子
范畴论——即数学家的接口——使用 , 弱幺半函子 “ 这个名字 ” 而 Haskell 语言——即程序员的接口——使用 , Applicative
类型类弱幺半函子要求我们提供一个单位态射 $\phi_0$ 和一个自然变换 $\phi_1$ 。 用于说明函子如何保持原范畴的幺半结构 , 而 ; Applicative
要求我们实现pure
和<*>
两个函数乍看上去 。 二者是如此不同 , 但我们会证明两套接口可以互相实现 , 因此它们是等价的 , 。
关于
内容安排
下面列出的大纲非常粗糙
范畴、 对象与态射
范畴的定义
后面我们会构造一个例子v :: T
v :: () -> T
函子: 范畴间的变换
函子的定义Maybe
[]
(->) s
逆变函子( 选读)
逆变(->) s
与 Op s
(s -> *)
(* -> s)
contramap :: (b -> a) -> f a -> f b
g >$< (f x)
自然变换: 函子间的变换
函子的自然变换safeHead :: [a] -> Maybe a
构造一个 []
到 Maybe
的自然变换
积与余积: 组合对象的两种方式
积和余积
极限与余极限( 选读)
选读内容()
以及对应的唯一态射族 const () :: a -> ()
Void
和对应的唯一态射族 absurd :: Void -> a
此外
幺半范畴
幺半范畴 (monoidal category)
笛卡尔闭范畴
笛卡尔闭范畴 (Cartesian closed category, CCC)a -> b
我们经常听到在函数式语言中函数是一等公民
作为对比
幺半群对象
幺半群对象 (monoid object)accumulate
的概念Applicative
和 Monad
的概念非常有帮助
准确地说(,)
诱导的笛卡尔积幺半结构Either
诱导的余积幺半范畴
弱幺半函子与 Applicative
弱幺半函子 (lax monoidal functor)Applicative
unit :: () -> f ()
和 strength :: (f a, f b) -> f (a, b)
pure
和 <*>
fmap
, unit
, strength
} 实现 {pure
, <*>
}
自函子范畴上的幺半群对象与 Monad
自函子范畴上的幺半群对象Monad
unit :: a -> f a
和 join :: f f a -> f a
return / pure
和 >>=
Monad
的联系
Freyd 范畴与 Arrow
( 选读)
选读内容Arrow
Applicative
和 Monad
之间的抽象
伴随函子
伴随函子 (adjoint functors)
作为例子(->) s
是 (s, )
的伴随curry
和 uncurry
Maybe
函子的伴随Monad
Monad
的例子(s, )
与指数函子 (->) s
复合而成的 monad 是 (s, ) -> s
State
monadMaybe
函子生成的就是 Maybe
monad[]
monad 也由类似的伴随函子生成而来State
monad(s, (->) s)
Store
comonadlens
库正是建立在这种结构上范畴论的世界观: 实例研究
插入小节Bool
类型相关的态射和对象Bool
类型的全部行为true :: () -> Bool
和 false :: () -> Bool
not :: Bool -> Bool
and :: (Bool, Bool) -> Bool
等态射val :: a
可以看成范畴幺元 ()
到对象 a
上的态射 val :: () -> a
f
加载实参 val
后的输出 f val :: b
看作态射复合 f . val :: () -> b
透镜与棱镜: 解析 lens
库
Profunctor Optics 和 lens
库Control.Lens.Prism
lens
的设计思想和实现Costate
comonad
米田引理
米田引理 (Yoneda lemma)
尾声: 形式化的边界
尾声
表面上看head :: [a] -> a
a
类型safeHead :: [a] -> Maybe a
就能轻松绕行——事实上要深刻得多
哥德尔不完备定理说明Maybe
或 Either
安置了所有异常
现实中的编程语言必须在二者间找到平衡
指 GPT 3.5 公开的时间点
在那之后, 一切面向文本输入输出的工作都在日渐失去价值, 这个说法最早可以追溯到知乎用户@酱紫君。 我非常认同, ↩︎。 除了库霸王
我似乎看不出有任何一个角色关心碧姬公主, 我甚至怀疑, 有些通关了很多代正作的人第一次知道碧姬公主是在, 马里奥赛车《 》 ↩︎。 由于我不是计算机系科班
而 SICP 是我的编程启蒙读物, 导致 Scheme 甚至是我的母语——或者换句话说, 我在启蒙阶段就是一个学杂了的双语者, Python 和 Scheme 都是我的母语, 我无法确定 SICP 多大程度塑造了我今天的思维。 那是我进入这个世界的第一扇门, 十年前。 我抬头看到门楣上刻着铭文, 抽象与隔离, 有一个声音一遍一遍问我准备好了吗, 我迫不及待地推开那扇门, 隆隆声扬起一阵呛人的灰尘, 然后我意识到门后是什么——现实世界汹涌的复杂性, 后来我逐渐明白。 那铭文是一个咒语, 我们用肉体凡胎试图理解——并对抗——比我们强大亿万倍的复杂性, 那是唯一的咒语, ↩︎。 人的工作记忆大约只能容纳 $7 \pm 2$ 个区块 (chunks)
怎样组织区块的内容并控制区块的数量, 这直接决定我们能处理多复杂的问题, ↩︎。 出于巧合
某天你初次尝试定义某种, 有限规模的容器“ 作为一个类型” 你定义了若干操作, 尝试让编译器帮你证明这些操作都保持你的容器有限, 起初一帆风顺。 但当你偶然写下某种递归操作, 你突然察觉到编译器表现出一些怪异的行为, 接着你开始反思, 是不是你设计的类型定义有问题, 也许你漏掉了什么——随着你反复迭代, 你潜意识中怀疑一切背后也许有什么神秘的东西, 似乎是某种你暂时说不清楚的限制, 并非 Haskell 语言给你的限制, 而是比那更宏大, 更深刻、 更接近某种事物的本源——你不知道如何表达这种感受、 于是你开始搜集资料, 终于, 你看到了这份讲义, ↩︎。