<译> 范畴,可大可小

6

原文见:http://bartoszmilewski.com/20...

-> 上一篇『类型与函数

可能你已经通过研究一些案例对范畴有所觉悟了,但是范畴是变化多端的,它可能会在你意想不到地方蹦出来。我们可以从很简单的东西上来观察它。

没有对象

最小的范畴是拥有 0 个对象的范畴。因为没有对象,自然也就没有态射。这个范畴挺悲哀的,因为它只拥有自己。不过,对于其它范畴而言,它可能是挺重要,例如所有范畴的范畴(对的,有这么一个范畴)。如果你觉得空集是有意义的,那么为何会觉得空的范畴无意义?

简单的图

用箭头将对象连接起来就可以构造出范畴。在一个有向图上增加一些箭头,就可以将它变成一个范畴。首先,要为每个结点增加恒等箭头。然后为任意两个首尾相邻的箭头(也就是任意两个可复合的箭头)增加一个复合箭头。每次添加一个新的箭头,你必须得考虑它本身与其他箭头(除了恒等箭头)的复合。你会画到自己实在不想画了,不过这样就足够了,这个有向图已经变成了范畴。

从另一个角度看这个过程,图中每个结点是一个对象,图中的边所构成就是态射。可以认为恒等态射就是长度为 0 的链。

这种由给定的图而产生的范畴,被称为自由范畴。它是一种自由构造的示例,即给定一个结构,用符合法则(在此,就是范畴论法则)的最小数量的东西来扩展它。接下来,会有更多的例子来说明这一点。

现在,截然不同的东西出现了!有这样一个范畴,它所包含的态射描述的是两个对象之间的关系——小于或等于。来检查一下它是不是一个真正的范畴。

Q:它有恒等态射吗?

A:每个对象都小于或等于它自身,通过!

Q:态射可以复合么?

A:如果 $a \le b$,$b \le c$,那么 $a \le c$,通过!

Q:态射遵守结合律么?

A:通过!

伴随这种关系的集合被称为前序集,因此一个前序集实际上是一个范畴。

也可以有一个更强的关系,它满足一个附加条件,即,如果 $a\le b$,$b\le a$,那么 $a$ 肯定等于 $b$。伴随这种关系的集合,叫偏序集。

最后,如果一个集合中的任意两个元素之间存在偏序关系,那么这种集合就叫做全序集。

可以将这些有序集描绘为范畴。前序集所构成的范畴,从任意对象 a 到任意对象 b 的态射最多只有一个。这样的范畴叫瘦范畴。

在一个范畴 C 中,从对象 a 到对象 b 的态射集被称为 hom-集,记为 C(a,b),有时也这样写 $Hom_C(a,b)$。前序集内的每个 hom-集要么是空集,要么是单例(Singleton)。在任一前序集构成的范畴内,C(a,a) 也是 hom-集,不过它肯定是个单例,只包含着恒等态射。前序集是允许出现环的,而这种东西在偏序集内则是禁止的。

弄清楚前序、偏序与全序是非常重要的,因为排序需要它们。像快速排序、桶排序、归并排序之类的排序算法,它们只能处理全序集。偏序集可以使用拓扑排序算法来处理。

作为集合的幺半群

幺半群(Monoid)是一个相当简单但是功能强大的概念。它是基本算数幕后的概念:只要有加法或乘法运算就可以形成幺半群。在编程中,幺半群无处不在。它们表现为字符串、列表、可折叠数据结构,并发编程中未来的一些东西,函数式响应编程中的事件,等等。

传统的幺半群被定义为伴有一个二元运算的集合。这个二元运算只需满足结合律。集合中包含着一个特殊的元素,对于这个二元运算,该元素的行为像一个返回其自身的 unit。

例如,包含 0 的自然数伴随着加法运算就可以形成一个幺半群。所谓的结合律,即:

$$ (a + b) + c = a + (b + c) $$

也就是说,在数字相加的时候,括号可忽略。

那个理想是永远保持中立的元素是 0,因为:

$$ 0 + a = a $$

以及

$$ a + 0 = a $$

第 2 个方程似乎是多余的,因为加法运算符合交换律,a + b = b + a,但是交换律并非幺半群的定义所需要。例如,字符串连接运算就不遵守交换律,但它可以构成幺半群。对于字符串连接运算,中立元素是空的字符串,它可以挂接到一个字符串的任意一侧,而后者依然面不改色。

在 Haskell 中,我们可以为幺半群定义一个类型类——一种包含着中立元素 mempty 并伴随二元运算 mappend 的类型:

class Monoid m where
    mempty :: m
    mappend :: m -> m -> m

具有两个参数的函数,其类型为 m -> m -> m,乍一看挺古怪,但是在我们懂得柯里化(Currying)之后,就能感受到这种形式的美。可以用两种基本方式来解释这多个箭头的意义:(1) 一个函数有多个参数,最右边的类型是返回值的类型;(2) 一个函数,它接受一个参数(最左边的那个),返回一个函数。在括号的帮助下,第二种解释可以被直观化为 m -> (m -> m),不过括号是多余的,因为箭头是从右向左结合的。过会儿再来关注这个问题。

注意,在 Haskell 中,无法解释 memptymappend 的幺半群性质,也就是说 mempty 是个什么样的中立者,mappend 符合怎样的结合律。因为这是程序猿的责任,毕竟 Haskell 不能未卜先知。

Haskell 里的类不像 C++ 的类那样咄咄逼人。当你定义一个新的类型时,不需要声明它所属的类。为一个给定的类型,声明它是某个类的实例,这种事可以向后延迟。例如,我们可以将 String 声明为一个幺半群,并为它提供 memptymappend 的实现(当然,在 Haskell 的标准库(Standard Prelude)里已经做了此事):

instance Monoid String where
    mempty = ""
    mappend = (++)

在此,我们重用了列表的连接运算 (++),因为 String 是列表,字符列表。

简单的说说 Haskell 的一个语法:任何中缀运算符,被括号围住之后,就可以转化为两个参数的函数。(在学习 Haskell 时,这可能是最难适应的东西。)

注意了啊,Haskell 允许函数相等。不过,在概念上,

mappend = (++)

与函数产生值时的相等

mappend s1 s2 = (++) s1 s2

是不同的。前者是 Hask 范畴(如果忽略底的话,是 Set)中态射的相等。像这样的方程不仅更简洁,也经常被泛化至其他范畴。后者被称为外延相等,陈述的是对于任意两个输入的字符串,mappend(++) 的输出是相同的。因为参数的值有时也称为point(情同:f 在点 x 处的值),外延相等也被称为 point-wise 相等。未指定参数的函数的相等,称为 point-free 相等。(顺便说一下,point-free 方程通常包含函数的复合,函数的复合所用的符号也是点,因此初学者可能会搞混了。)

要用现代 C++ 语言来声明一个幺半群,只能用概念语法(C++ 标准提案):

template<class T>
  T mempty = delete;

template<class T>
  T mappend(T, T) = delete;

template<class M>
  concept bool Monoid = requires (M m) {
    { mempty<M> } -> M;
    { mappend(m, m); } -> M;
  };

第一个定义是使用一个值的模板(也是提案)。一个多态的值是一个值的族——每个类型的不同值。

关键词 delete 的意思是没有默认值,不得不根据具体情况给它具体的值。这与 mappend 相似。

概念 Monoid 是一个谓词,对于给定的类型 M,它测试是否存在合适的 memptymappend 的定义。

通过提供合适的特化与重载便可建立幺半群概念的实例:

template<>
std::string mempty<std::string> = {""};

std::string mappend(std::string s1, std::string s2) {
    return s1 + s2;
}

幺半群作为范畴

集合形式的幺半群,现在我们知道了。但是你知道的,在范畴论中,我们所尝试的事情是放弃集合,我们要讨论的是对象与态射。因此,我们的视角应当改变一下,从范畴的角度来看作用于集合的『移动』或『转移』二元运算。

例如,有一个将每个自然数都加 5 的运算,它会将 0 映射为 5,将 1 映射为 6,2 映射为 7,等等。这样就在自然数集上定义了一个函数,挺不错的,我们有了一个函数与一个集合。通常,对于任意数字 n,都会有一个加 n 的函数—— n 的『adder』。

这些 adder 们如何复合?加 5 的函数与加 7 的函数复合起来,是加 12。因此 adder 们的复合等同于加法规则。这也很好,我们可以用函数的复合来代替加法运算。

等一下,事情还没完:还有一个 adder 是面向中立元素 0 的。加 0 不会改变任何东西,因此它是自然数集上的恒等函数。

即使不以传统的加法规则作为参照,照样能给出 adder 们的复合规则。注意,adder 们的复合是符合结合律的,因为函数的复合是符合结合律的,而且我们也有个加 0 的函数作为恒等函数。

敏锐的读者可能会注意到,从整型到 adder 的映射符合 mappend 类型签名的第二种解释,即 m -> (m -> m)。这意味着 mappend 将幺半群的一个元素映射为作用于这个集合的一个函数。

现在,我希望你忘掉你在处理自然数集,只是将它视为一个单一的对象,它伴随着一捆态射——adder 们。一个幺半群,是一个单对象的范畴。事实上,幺半群的名字来自希腊语 mono,它的意思是单个的。每个幺半群都能被表述为带有一个态射集的单对象范畴,这些态射皆符合复合规则。

幺半群范畴

字符串的连接是一个有趣的例子,因为我们要选择是定义左 appender,还要定义右 appender。这两个态射是彼此镜象的。很容易确定这一点,将「bar」挂到「foo」的右侧,相当于将「foo」挂到「bar」的左侧。

你可能会问,是否每个范畴化的幺半群都会定义一个唯一的伴随二元运算的集合的幺半群。事实上我们总是能够从单个对象的范畴中抽出一个集合。这个集合是态射的集合——在前面的例子里就是 adder 们。换句话说,对于只含单个对象 m 的范畴 M,我们有一个 home-集 M(m, m)。在这个集合上,我们很容易定义一个二元运算:两个元素相乘相当于两个态射的复合。如果你给我 M(m, m) 中的两个元素 fg,它们的乘积就相当于 g∘f。复合总是存在的,因为这些态射的源对象与目标对象是同一个对象。这种乘法运算也符合范畴论法则中的结合律。恒等态射也是肯定存在的。因此,我们总是能够从幺半群范畴中复原出幺半群集合。无论从哪个角度来说,它们都是同一个东西。

同态集

幺半群的 hom-集看上去是态射,也是点集。

对于数学家的挑剔而言,有点小 bug:态射不必形成集合。在范畴的世界里,有比集合更大的东西。一个范畴,其中任意两个对象之间的态射们形成一个集合,这样的范畴是局部小的。不过,因为承诺不要太数学,所以我会忽略这些细枝末节,在讲 Haskell 的『记录』语法时,再谈论它们。

范畴论中大量的有趣现象都来自于:home-集里的元素可被视为遵守复合法则的态射,也可被视为集合中的点。在此,M 中的态射的复合就会变成集合 M(m,m) 中的幺半群式的乘法运算。

致谢

感谢 Andrew Sutton 根据他和 Bjame Stroustrup 最新的提案,重写了我的 C++ 幺半群概念代码。

挑战

  1. 从下面的东西生成自由范畴:(1) 有一个结点,没有边的图;(2) 有一个结点并且有一条边(有方向)的图;(3) 有两个结点并且二者之间有一条边的图;(4) 有一个结点,有 26 个箭头并且每个箭头标记着字母表上的一个字母的图。
  2. 这是哪种序?(1) 伴随着包含关系的一组集合的集合;(2) 伴随着子类型关系的 C++ 的类型构成的集合。
  3. Bool 是两个值的集合,看看它能不能分别与 &&|| 构成幺半群(集合理论中的)。
  4. 用 AND 运算表示 Bool 幺半群:给出态射以及它们的复合法则。
  5. 将 (加 3) 与 (模 3)的复合表示为幺半群范畴。

-> 下一篇:『Kleisli 范畴


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

views63 · 2015年10月27日

但是在我们懂得丘奇化(Curring)之后……

“Curring” 好像翻译成“柯里化”比较多

+1 回复

Adam_Niu · 2016年06月17日

多谢翻译~另外是currying不是curring吧

+1 回复

garfileo 作者 · 2015年10月27日

改了。本来就是柯里,但当时在看王垠写的丘奇与图灵时,不小心就笔误了。

回复

views63 · 2015年10月27日
  1. 将加上 3 的模表示为幺半群范畴。

这句这样翻译不好理解,我暂时也没想到怎么表述比较好。觉得这里应该突出“加法”(二元关系)模3同余的集合(对象)

回复

garfileo 作者 · 2015年10月27日

是加 3 与模 3 的复合所形成的二元运算。

回复

views63 · 2015年10月27日

这样就清晰了。嗯,我理解错了。

回复

bamzy · 2017年02月01日

那段话是说, haskell是先定义好类型(type), 然后声明它属于哪个class, 同时定义这个class必备的函数, "instance monoid string where…"那段就是声明string类型是monoid类型类的实例, 定义它的mempty和mappend函数(理论上前文已经定义好了String)…当然实际上不能这样写你也不能定义String类型, 因为在"标准库"prelude里已经这样定义过了, 这里只是举例用…

回复

leo_liu · 2017年11月25日

越来越难懂了,需要补很多的数学概念

回复

载入中...