原文:https://bartoszmilewski.com/2...
上一篇:函数类型
译注:由于距离之前的翻译的时间太久,所以有些内容可能有点不太相符。待我找时间校对。
我们已经讨论过,函子可以在维持范畴结构的前提下实现范畴之间的映射。函子可以将一个范畴嵌入到另一个范畴,它也可以让多个范畴坍缩为一个范畴且不会破坏范畴的结构。凭借函子,我们可以在一个范畴之内构建另一个范畴。源范畴可视为目标范畴的部分结构的模型或蓝图。
将一个范畴嵌入到另一个范畴可能有许多种方式。有时这些方式是等价的,有时它们不等价。你可以将整个的范畴坍缩为另一个范畴中的一个对象,也可以将一个范畴中的每个对象映射为另一个范畴中不同的对象,将前者中的每个态射映射为后者中的不同的态射。同样的想法可以有多种不同方式的实现。自然变换可以帮助我们对比这些实现。自然变换是函子之间的映射——可以保持函子性质不变的特殊映射。
对于范畴 C
与 D
之间的两个函子 F
与 G
,如果我们只关注 C
中的一个对象 a
,它被映射为 D
中的两个对象:F a
与 G a
,那么应该存在一个函子映射,它可以将 F a
映射为 G a
。
由于在同一范畴中的对象映射应该不会脱离该范畴,而且我们不想人工建立 F a
与 G a
的联系,因此很自然的考虑使用既有的联系——所谓的态射。自然变换本质上是如何选取态射:对于任意对象 $a$,自然变换就是选取一个从 F a
到 G a
的态射。如果将一个自然变换称为 α
,那么这个态射就被称为在 a
上的 α
分量(Component of α
at a
),记作 α_a
:
α_a :: F a -> G a
译注:
α_a
表示 $\alpha_a$。用 HTML 标记来表示,就是α<sub>a</sub>
。由于许多号称支持 Markdown 文档格式的网站,它们不支持在 Markdown 里自由使用 HTML 标记。虽然这些网站有一些也支持 TeX 公式,但是又没法在代码排版环境中嵌入 TeX 公式。因此,我不得不使用下划线来表示公式里的下标。在下文,我也会使用^
来表示上标,顺便说一句,我对现在几乎任何一个由用户自行发布内容的网站都不满意。
记住,a
是一个在 C
中的对象,而 α_a
是 D
中的一个态射。
对于某个 a
,如果在 F a
与 G a
之间没有态射,那么 F
与 G
之间也就不存在自然变换。
故事刚刚说了一半。函子所映射的不止是对象,它们也能映射态射,自然变换应该如何对待这种映射?答案是,态射的映射是固定的——在 F
与 G
之间的任何一个自然变换下,F f
必须变换成 G f
。也就是说,两个函子对态射所形成的映射会彻底限制与之相适的自然变换的定义。来考虑在范畴 C
中的两个对象 a
与 b
之间的态射 f
,它被映射为范畴 D
中的两个态射 F f
与 G f
:
F f :: F a -> F b
G f :: G a -> G b
自然变换 α
提供两个附加的态射来补全 D
中的结构:
α_a :: F a -> G a
α_b :: F b -> G b
现在,我们便有了两个从 F a
到 G b
的途径。为了保证这两种途径等价,必须引入自然性条件(Naturality condition):
G f ∘ α_a = α_b ∘ F f
这个条件应该对于任意的 f
都成立。
自然性条件非常有用。例如,如果态射 F f
是可逆的,自然性决定了以 α_a 形式表示的 α_b
。通过 f
,基于自然性条件可将 α_a
变换为:
α_b = (G f) ∘ α_a ∘ (F f)^(-1)>
如果两个对象之间存在多个可逆的态射,上述变换也都成立。尽管态射通常是不可逆的,但是我想说的是两个函子之间的自然变换并非一定存在。因此,与自然变换相关的函子的多寡,可在很大程度上显现这些函子所操纵的范畴的结构。在讲极限与 Yoneda 定理时,我会给出一些例子。
从分量的角度来看自然变换,您可以认为它将对象映射为态射。但是,从自然性条件的角度来看,你也可以认为它将态射映射为四方形的交换图(Commuting squares)—— C 中的每个态射都被映射为 D 中的一个交换图。
自然变换的这一性质可以让很多范畴便于构造,这些范畴往往包含着这类的交换图。在正确选择函子的情况下,大量的交换条件都能够转换为自然性条件。以后在讲到极限、上极限(Colimit)以及伴随(Adjunction)时会给出一些例子。
最后,自然变换可用于定义函子的同构。如果说两个函子是自然同构的,差不多是在是说它们是相同的函子。自然同构是以自然变换的形式定义的,这种自然变换的各个分量都是同构的(可逆的态射)。
多态函数
之前讲过函子(更确切的说,是自函子)在编程中所扮演的角色。它们相当于类型构造子——将类型映射为类型。不过,函子也能将函数映射为函数,这种映射是通过一个高阶函数 fmap
(在 C++ 中则是 transform
, then
之类的行为)。
为了构造一个自然变换,我们从一个对象开始,也就是一种类型,设为 a
。一个函子 F
,可以将 a
映射为类型 F a
。另一个函子 G
,可以将 a
映射为 G a
。在 a
上的自然变换 alpha
的分量是一个从 F a
到 G a
的函数。使用用伪 Haskell 代码,可将其表示为:
alpha_a :: F a -> G a
自然变换是面向各种类型 a
的多态函数:
alpha :: forall a . F a -> G a
forall a
在 Haskell 中是可选的(可以用语言扩展 ExplicitForAll
开启它)。通常,可将其写为:
alpha :: F a -> G a
请记住,这是由 a
参数化的一个函数族。这是 Haskell 语法简洁性的又一个示例。在 C++ 中,与之类似的构造要麻烦一些:
template<Class A> G<A> alpha(F<A>);
Haskell 的多态函数与 C++ 的泛型函数之间存在很大的区别,主要体现为函数的实现方式以及类型检查方式上。在 Haskell 中,一个多态函数必须对于所有类型是唯一的。一个公式必须适用于所有的类型。这就是所谓的参数化多态(Parametric polymorphism)。
C++ 默认提供的是特设多态(Ad hoc polymorphism),这意味着模板不一定涵盖所有类型。一份模板是否适用于某种给定的类型需要在实例化时方能确定,彼时,编译器会用一种具体的类型来替换模板的类型参数。类型检测是以推导的形式实现的,编译器经常会给出难以理解的错误信息。
在 C++ 中,还有一种函数重载与模板特化机制,通过这种机制可以为不同的类型定义函数的不同版本。Haskell 也有类似的机制,即类型类(Type class)与类型族(Type family)。
Haskell 的参数化多态有一个一个不可预料的结果。凡是像这种类型的多态函数:
alpha :: F a -> G a
函子 F
与 G
自动满足自然性条件。这里,我们再回到范畴论的概念(f
是一个函数,f::a->b
):
G f ∘ α_a = α_b ∘ F f
在 Haskell 中,函子 G
作用于一个态射 f
是通过 fmap
实现的。可使用 Haskell 伪码将上述概念表示为:
fmap_G f . alphaa = alphab . fmap_F f
归功于 Haskell 的类型推导,上述类型标记是不需要的,因此可写为以下形式:
fmap f . alpha = alpha . fmap f
这依然不是真正的 Haskell 代码——在代码中无法表示函数等式——不过,上式是恒等的,程序员在等式推导中可以使用这个公式,此外,编译器也可以利用这个公式对代码进行优化。
自然性条件之所以在 Haskell 里会自动被满足,这是『免费的定理』的自然结果。在 Haskell 里,参数化多态,将其用于定义自然变换,会引入非常强的限制条件——一个公式适应所有类型。这些限制条件会变成面向这些函数的方程一样的定理。对于能够对函子进行变换的函数,免费的定理是自然性条件。(作者注:你可以阅读我写的另一篇文章《Parametricity: Money for Nothing and Theorems for Free》,会让你对这些免费的定理能够了解得更多一些。)
之前我提到过,在 Haskell 里,可以将函子视为泛型容器。我们可以继续这个类比,将自然变换视为一种重组方法,即将一个容器里的东西取出来放到另一个容器里。我们不会触碰这些东西:不修改,也不创造。我们只是将它们(或它们的一部分)复制到新的容器里,在这个过程中有时候会对它们作几次乘法。
于是,自然性条件就是,首先它不关心我们是先通过 fmap
修改这些东西,然后再将它们放到新容器里,还是先把它们放到新容器里再用适用于这个容器的 fmap
去修改它们。重组与 fmap
,它们是正交的,『你走你的阳关道,我过我的独木桥』。
来看一下 Haskell 里的自然变换。首先来看列表与 Maybe
这两种函子之间的自然变换,它的功能是,当且仅当列表非空时返回列表的首元素:
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just x
它是面向 a
的函数多态化。它可以不受限制地作用于任意一种类型 a
,因此它是一个参数化多态的例子。从而,它就是两个函子之间的一个自然变换。不过,现在这只是我们在自以为是,下面来验证它是否符合自然性条件。
fmap f . safeHead = safeHead . fmap f
我们要考虑两种情况;一个空列表:
fmap f (safeHead []) = fmap f Nothing = Nothing
safeHead (fmap f []) = safeHead [] = Nothing
和一个非空列表:
fmap f (safeHead (x:xs)) = fmap f (Just x) = Just (f x)
safeHead (fmap f (x:xs)) = safeHead (f x : fmap f xs) = Just (f x)
上面我动用了面向列表的 fmap
:
fmap f [] = []
fmap f (x:xs) = f x : fmap f xs
与面向 Maybe
的 fmap
:
fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)
当函子之一是微不足道的 Const
函子的时候,会发生一件有趣的事。一个以 Const
函子为始点或终点的自然变换,看上去就像一个函数,它即面向它的返回类型多态,也面向它的参数类型多态。
例如,可将 length
视为从列表函子到 Const Int
函子的自然变换:
length :: [a] -> Const Int a
length [] = Const 0
length (x:xs) = Const (1 + unConst (length xs))
这里,unConst
用于剥除 Const
构造子:
unConst :: Const c a -> c
unConst (Const x) = x
当然了,实际上 length
的定义是下面这样:
length :: [a] -> Int
这个定义有效地掩盖了 length
作为自然变换的本质。
寻找一个以 Const
函子为始点的参数化多态函数有点难,因为这需要无中生有创造一个值出来。我们所能想到的最好的办法是:
scam :: Const Int a -> Maybe a
scam (Const x) = Nothing
还有一个不同寻常的函子,我们之前已经见过它了,它在 Yoneda 引理中扮演了重要的角色。这个函子就是 Reader
函子。下面我用 newtype
来重写一下它的定义:
newtype Reader e a = Reader (e -> a)
这个函子被两种类型参数化了,但是它的(逆变)函子性仅着落在第二个类型上:
instance Functor (Reader e) where
fmap f (Reader g) = Reader (\x -> f (g x))
对于每种类型 e
,都可以定义从 Reader e
到任何其他函子的自然变换家族。以后会看到,这个家族的成员总是与 f e
的元素壹壹对应(Yoneda 引理)。
例如,考虑有时会被忽略的仅有一个值 ()
的 unit 类型 ()
。函子 Reader ()
接受任何一种类型 a
,将它射入函数类型 () -> a
。这些函数可以从集合 a
中拮取一个元素。这些函数的数量与 a
中元素的数量一样多。现在,来看一下从这种函子到 Maybe
函子的自然变换:
alpha :: Reader () a -> Maybe a
这样的自然变换只有 dumb
:
dumb (Reader _) = Nothing
与 obvious
:
obvious (Reader g) = Just (g ())
(用 g
能做的事情仅仅是让它作用于 ()
。)
还有,实际上按照 Yoneda 引理的说法,这些与 Maybe ()
类型的两个元素相符,即 Nothing
与 Just ()
。呆会儿我们就会再回到 Yoneda 引理上来——以上的说法有些不严肃。
超自然性
两个函子之间的参数化多态函数(包括 Const
函子这种边界情况)必定是自然变换。因为所有的标准代数数据类型都是函子,在这些类型之间的任何一个多态函数都是自然变换。
我们还掌握了函数类型,它们对于它们的返回类型而言具有着函子性。我们可以使用它们来构造函子(例如 Reader
函子),并为这些函子构造自然变换——更高阶的函数。
不过,对于参数类型而言,函数类型不具备协变性,它们具备逆变性。当然,逆变函子就是相反范畴中的协变函子。在范畴意义上,两个逆变函子之间的多态函数依然可视为自然变换,除了它们只能作用于 Haskell 类型所构成的两个相反的范畴里的函子。
你可能还记得之前我们见过的一个逆变函子的示例:
newtype Op r a = Op (a -> r)
这个函子对于 a
而言具有逆变性:
instance Contravariant (Op r) where
contramap f (Op g) = Op (g . f)
我们可以写一个函数,假设它从 Op Bool
到 Op String
:
predToStr (Op f) = Op (\x -> if f x then "T" else "F")
由于这两个函子不具备协变性,它并非 Hask 范畴中的自然变换。不过,由于它们都具备逆变性,所以它们满足『相反的』自然性条件:
contramap f . predToStr = predToStr . contramap f
注意,函数 f
必须得走与 fmap
的作用下的方向相反的方向,因为 contramap
的签名是:
contramap :: (b -> a) -> (Op Bool a -> Op Bool b)
存在不是函子的类型构造子吗,它是协变的还是逆变的?看下面的例子:
a -> a
这不是一个函子,因为同一类型的 a
出现在负(逆变)位与正(协变)位上。对于这种类型,fmap
或 contramap
都无法实现。因此,函数签名:
(a -> a) -> f a
其中 f
是任意函子,这个函数不是自然变换。有趣的是,存在着一种广义的自然变换,叫作双自然变换,它们能够处理这些情况。在我们讨论端(End)的时候会遇到它们。
函子范畴
现在,我们有了函子之间的映射——自然变换——因此很自然地就会想到,函子是否能够形成范畴?没错,它们可以。对于每对范畴而言,仅存在一个函子范畴。在这个范畴里,对象是从 C 到 D 的函子,而态射就是这些函子之间的自然变换。
我们必须得定义两个自然变换的复合,不过,这相当容易。自然变换的分量是态射,而我们知道怎样实现态射的复合。
没错,我们以从函子 F
到函子 G
的自然变换 α
为例。它在对象 a
上的分量是某个态射:
α_a :: F a -> G a
我们打算用对 α
与 β
进行复合,后者是从 G
到 H
的自然变换。β
在 a
上的分量是一个态射:
β_a :: G a -> H a
这些态射是可复合的,复合结果又是一个态射:
β_a ∘ α_a :: F a -> H a
可以用这个态射作为自然变换 $β\cdot α$ 的分量——自然变换 β
在 a
之后的复合:
(β ⋅ α)_a = β_a ∘ α_a
仔细观察上面这幅图,可以确信这种复合的结果是从 F
到 H
的自然变换:
H f ∘ (β ⋅ α)_a = (β ⋅ α)_b ∘ F f
自然变换的复合遵循结合律,因为它们的分量都是常规态射,而后者的复合是遵循结合律的。
最后,对于每个函子 F,存在一个恒等自然变换 1_F,它的分量是恒等态射:
id_{F a} :: F a -> F a
所以,函子的确能形成范畴。
说一说记法。与 Saunders Mac Lane 一样,上面我使用小圆点(dot)来表示各种自然变换的复合。问题是存在两种自然变换的复合方式。一种叫竖向复合,因为在示意图里,函子通常是往下堆砌的。竖向复合对于定义函子范畴很重要。下面我简短介绍一下横向复合。
范畴 C 与 D 之间的函子范畴记为 Fun(C, D)
或 [C, D]
,有时也写成 D^C
。最后这种记法暗示了可将函子范畴本身视为其他范畴里的一个函数对象(指数)。实际上是这样吗?
看一下我们到现在为止所构建的抽象层。我们从一个范畴开始,它由一组对象与态射构成。范畴本身(或严格地说是小范畴,它们的对象形成集合)是更高层的范畴 Cat 里的对象。在 Cat 里,态射是函子。Cat 里的 Hom-集是函子构成的集合。例如,Cat(C,D)
是范畴 C 与 D 之间的函子集合。
函子范畴 [C, D]
也是两个范畴之间的函子集合(加上自然变换为态射)。它里面的对象也是 Cat(C,D)
里的东西。此外,函子范畴是范畴,它本身必须得是 Cat 里面的对象之一(也就是说,两个小范畴之间的函子范畴本身也很小)。一个范畴里的 Hom-集与同一个范畴里的对象之间存在联系。这种情况就像我们在上一节里所看到的指数形式的对象。现在来看一下,Cat 里如何构造后者。
你可能还记得,为了构造一个指数,需要首先定义积。在 Cat 里,定义积相当容易,因为小范畴是对象的集合,而我们又知道怎样定义集合的笛卡尔积。因此,积范畴 C × D
里的一个对象,是两个对象构成的序对 (c, d)
,一个来自 C
,一个来自 D
。类似地,在这样的序对 (c, d)
与 (c', d')
之间的态射是一个态射序对 (f, g)
,其中 f :: c -> c'
,g :: d -> d'
。由于这些态射序对由 C
与 D
中态射组成,因此总是会有一个由 C
和 D
中的恒等态射构成的序对。长话短说,Cat 是一个完全的笛卡尔闭范畴,对于任意一对范畴(译注:例如 C
与 D
),它里面存在着相应的指数对象 D^C
。由于我说过, Cat 里的对象是范畴,因此 D^C
是范畴,它就是 C
与 D
之间的函子范畴。
2-范畴
问题解决了,我们现在近观一下 Cat。根据定义,Cat 里的任意 Hom-集都是函子集合。但是,就像我们见识过的,两个对象之间的函子有着比集合更丰满的结构。它们形成一个范畴,以自然变换为态射。由于在 Cat 里,函子被认为是态射,自然变换就是态射之间的态射。
这个更丰满的结构是 2-范畴的一个例子。2 -范畴是一个广义的范畴,其中,除了对象和态射(这里应该叫它 1-态射)之外,还有 2-态射,它就是态射之间的态射。
Cat 的 2-范畴具有:
- 对象:(小)范畴
- 1-态射:范畴之间的函子
- 2-态射:函子之间的自然变换
我们用 Hom-范畴——函子范畴 D^C
来代替范畴 C
与 D
之间的 Hom-集。我们有常规的函子复合:来自 D^C
的函子 F
与来自 E^D
的函子 G
复合,可以得到来自 E^C
的函子 G ∘ F
。但是,在每个 Hom-范畴内部,也存在着复合——函子之间自然变换或 2-态射的竖向复合。
使用 2-范畴里的两种复合方法,问题升级为:它们之间有何关系?
我们先选在 Cat 里选两个函子,或者 1-态射:
F :: C -> D
G :: D -> E
与它们的复合:
G ∘ F :: C -> E
假设我们有两个自然变换,α
与 β
,它们分别作用于函子 F
与 G
:
α :: F -> F'
β :: G -> G'
注意,我们不能对这两个自然变换应用竖向复合,因为 α
的终点与 β
的始点不重合。实际上,它们分别属于两个不同的函子范畴 D^C
与 E^D
。不过,我们能够复合函子 F'
与 G'
,因为 F'
的终点与 G'
的起点重合——它就是范畴 D
。函子 G’∘ F’
与 G ∘ F
之间有什么关系呢?
现在我们手里有 α
与 β
,可不可以定义一个从 G ∘ F
到 G’∘ F’
的自然变换?我们画个草图看看:
往常,我们从 C
中的一个对象 a
开始。它分裂为两个 D
中的对象:F a
与 F' a
。还有一个态射,α
的一个分量,连接这两个对象:
α_a :: F a -> F'a
在从 D
到 E
的时候,这两个对象进一步分裂为四个对象:
G (F a), G'(F a), G (F'a), G'(F'a)
我们还有 4 个态射,它们形成了一个方格。这些态射中有两个是自然变换 β
的分量:
β_{F a} :: G (F a) -> G'(F a)
β_{F'a} :: G (F'a) -> G'(F'a)
另外两个则是 α_a
在两个函子下的图像(函子映射了态射):
G α_a :: G (F a) -> G (F'a)
G'α_a :: G'(F a) -> G'(F'a)
好多态射。我们的目标是寻找从 G (F a)
到 G'(F'a)
的态射。一个候选解是连接两个函子 G ∘ F
与 G’∘ F’
的自然变换的分量。事实上,从 G (F a)
到 G'(F'a)
的路径不是一条,而是两条:
G'α_a ∘ β_{F a}
β_{F'a} ∘ G α_a
幸好,它们相等。因为这四个态射所形成的方格对于 β
而言具有自然性。
我们刚才已经定义了从 G ∘ F
到 G’∘ F’
的自然变换的一个分量。倘若你足够有耐心,那么这个变换的自然性证明相当直观。
我们将这个自然变换称为 α
与 β
的横向复合:
β ∘ α :: G ∘ F -> G'∘ F'
再一次沿用 Mac Lane 的记法,我使用小圆圈来表示横向复合。在它出现的位置上,你也可能看到的是星号。
有一个范畴化的经验法则:每次拿到复合时,应该去找一个范畴。我们有自然变换的竖向复合,它是函子范畴的一部分。那么,横向复合呢?它身居什么范畴里?
要得到答案,需要从侧面来看 Cat。不要将自然变换看成函子之间的箭头,而是将它们看成范畴之间的箭头。一个自然变换位于两个范畴之间,而这两个范畴原本是由这个自然变换所变换的函子连接的。我们可以认为这个自然变换连接着这两个范畴。
现在,我们把注意力放在 Cat 里的两个对象上,即范畴 C
与 D
。存在着由自然变换构成的集合,这些自然变换来往于连接 C
与 D
的函子之间。这些自然变换就是我们从 C
到 D
的新箭头。同理,也有一些自然变换来往于连接 D
与 E
的函子之间的自然变换,我们将它们视从 D
到 E
的新箭头。横向复合,就是这些箭头的复合。
我们也有一个从 C
到 C
的恒等箭头。它是 C
上的恒等函子自身的恒等自然变换。注意,横向复合的恒等也是竖向复合的恒等,但是反过来却不是。
最后,这两种复合满足交换律:
(β' ⋅ α') ∘ (β ⋅ α) = (β' ∘ β) ⋅ (α' ∘ α)
在此,我引用 Saunders Mac Lane 的说法:读者可能觉得绘制显而易见的示意图要比去证明它更有趣。
以后还会有更多的记法。在这个从侧面看 Cat 的新解释里,从一个对象到另一个对象存在两种方法:使用函子或使用自然变换。然而,我们可以将函子箭头重新解释为一种特殊的自然变换:恒等自然变换作用于这个函子。因此,你将会经常遇到这种记号:
F ∘ α
其中,F
是从 D
到 E
的函子,而 α
是从 C
到 D
的两个函子之间的自然变换。因为你不能将用一个自然变换去与一个函子进行复合,所以这个记号需要解读为恒等自然变换 1_F
位于 α
之后的横向复合。
类似地:
α ∘ F
是 α
位于 1_F
之后的横向复合。
总结
这是对这本书第一部分的总结。我们已经了解了范畴论的基本术语。你可能会这样认为:对象与范畴是名词;态射、函子以及自然变换是动词。态射连接了对象,函子连接了范畴,自然变换连接了函子。
但是我们已经看到了,一个抽象层上的一个动作,在下一个抽象层次上就变成了一个对象。态射的集合变成了函数对象。作为对象,它可以是另一个态射的始点或终点。这就是高阶函数背后的思想。
函子,将对象映射为对象,因此我们将其作为类型构造子,或者作为一种参数化的类型。函子,也能将态射映射为态射,因此它也是高阶函数——fmap
。有一些简单的函子,例如 Const
、积以及余积,它们可以产生大量的代数数据类型。函数类型也具有函子性,协变性与逆变性,它们可以用于扩充代数数据类型。
函子在函子范畴里可以视为对象。这样,它们就变成了态射的始点与终点,于是有了自然变换。自然变换就是特定形式的多态函数。
挑战
- 定义从
Maybe
到列表函子的自然变换,并证明它符合自然性条件。 - 为
Reader ()
与列表函子构造至少两个不同的自然变换。存在多少个不同的()
列表? - 用
Reader Bool
与Maybe
来做上一个练习。 - 揭示自然变换的横向复合满足自然性条件(提示:使用分量)。对于追求示意图的人而言,这是个很好的练习。
- 写一篇文章,谈谈你是如何觉得画显而易见的示意图胜过证明交换律。
- 为不同的
Op
函子之间的变换的相反自然性条件,创建一些测试案例。以下是一个示例:
op :: Op Bool Int
op = Op (\x -> x > 0)
与
f :: String -> Int
f x = read x
致谢
感谢 Gershom Bazerman 检查了我的数学和逻辑,也感谢 André van Meulebrouck 在编辑方面的帮助。
$$ \cdot $$
译注:作者后面还有两部分内容,但是我决定只翻译到这里。另外,这篇文章的原文下面的评论区,一位网名「benjaminy」的人的评论,也值得思考。另外,作者在这第一部分所讲述的这些范畴论概念,已经足以让你在一定程度上了解单子——自函子范畴上的一个幺半群。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。