<译> 范畴,可大可小

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

-> 上一篇『类型与函数算法

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

没有对象

最小的范畴是拥有 0 个对象的范畴。由于没有对象,天然也就没有态射。这个范畴挺悲哀的,由于它只拥有本身。不过,对于其它范畴而言,它多是挺重要,例如全部范畴的范畴(对的,有这么一个范畴)。若是你以为空集是有意义的,那么为什么会以为空的范畴无心义?segmentfault

简单的图

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

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

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

如今,大相径庭的东西出现了!有这样一个范畴,它所包含的态射描述的是两个对象之间的关系——小于或等于。来检查一下它是否是一个真正的范畴。函数

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 范畴

相关文章
相关标签/搜索