原文见 http://bartoszmilewski.com/20...c++
上一篇文章,即《写向程序猿的范畴论》的序言,发布以后获得的正面反馈让我有些不知所措。同时,它也激励了我,由于我感觉到了你们付诸于个人殷切指望。不过,我担忧的是不管我如何努力,依然众口难调。有些读者但愿这本书偏于现实,有些人则但愿它能抽象一些。有些憎恨 C++ 的人但愿全部的示例都是 Haskell 的,而那些憎恨 Haskell 的人又但愿示例是 Java 的。我还知道内容的进展对于有些人可能太慢了,而对于有些人可能又太快了。这本书可能不会很完美,它会充满着妥协。不过,我只指望可以与你们分享一下我顿悟时的惊喜。咱们如今从最基本的东西开始。程序员
范畴是一个至关至关至关简单的概念。一些对象以及对象之间存在的一些箭头就构成了一个范畴。因此,范畴很容易用图形来表示。对象能够画成圆或点,箭头就画成箭头。为了好玩,有时我会把对象画成小猪,将箭头画成焰火。范畴的本质是复合,若是你愿意,也能够说复合的本质是范畴。箭头能够复合,所以若是你有一个从 A 指向 B 的箭头,又有一个从 B 指向 C 的箭头,那么就一定有一个复合箭头——从 A 指向 C 的箭头。编程
在范畴论中,若是有一个箭头从 A 指向 B,又有一个箭头从 B 指向 C,那么就一定存在一个从 A 指向 C 的箭头,它是前两个箭头的复合。这幅图并不是一个完整的范畴,由于它没有自态射(详见后文)。segmentfault
如今你已经凌乱了么?不要绝望。如今来点实在的,将箭头想象为函数,虽然它的学名叫态射。你有一个函数 f,它接受一个 A 类型的值,返回一个 B 类型的值。你还有一个函数 g,它接受一个 B 类型的值,返回一个 C 类型的值。你能够将 f 的返回值传递给 g,这样就完成了这两个函数的复合,你获得的是一个新的函数,它接受一个 A 类型的值,返回一个 C 类型的值。bash
在数学中,这样的复合能够用一个小圆点链接两个函数来表示,即 g∘f. 注意,复合是从右向左发生的。有些人可能仍是有点不理解。你可能熟悉 Unix 中的管道,例如ide
$ lsof | grep Chrome
也可能熟悉 F# 语言中的 >>
,它们都是从左向右传递信息的。可是在数学与 Haskell 中,函数的复合是从右向左传递信息。若是你将 g∘f 读做 g after f 可能会有助于理解。函数式编程
如今咱们来写一些 C 代码。咱们有一个函数 f,它接受 A 类型的参数值,返回一个 B 类型的值:函数
B f(A a);
还有一个测试
C g(B b);
那么这两个函数的复合,就是:网站
C g_after_f(A a) { return g(f(a)); }
此次,你能够看到 C 中的从右向左的的复合:g(f(a))
。
我但愿 C++ 标准库中存在一个模板,它可以接受两个函数而后返回它们的复合,可是惋惜并无这样的模板。因此咱们只能试试 Haskell 了。下面是一个从 A 到 B 的函数的声明:
f :: A -> B
相似的还有
g :: B -> C
它们复合为:
g . f
一旦你见识到 Haskell 是这么的简单,就会以为 C++ 在函数概念的直接表达方面显得有些无能了。Haskell 也支持使用 Unicode 字符来写函数的复合:
g ∘ f
也可使用 Unicode 字符来写冒号与箭头:
f ∷ A → B
这就是咱们的 Haskell 第一课:两个冒号的意思是『类型为……』。一个函数的类型是由两个类型中间插入一个箭头而构成的。要对两个函数进行复合,只需在两者之间插入一个 .
(或者 Unicode 小圆圈)。
在任何范畴中,复合必须知足两个很是重要的性质:
1. 复合是可结合的(结合律)。若是你有三个态射,f,g 与 h,它们可以被复合(也就是它们的对象可以首尾相连),那么你就不必在复合表达式中使用括号。在数学中,可表示为:
h∘(g∘f) = (h∘g)∘f = h∘g∘f
在 Haskell 伪代码(之因此说『伪』,是由于 Haskell 没有为函数的相等进行定义)中,可表示为:
f :: A -> B g :: B -> C h :: C -> D h . (g . f) == (h . g) . f == h . g . f
对于函数的处理,结合律至关清晰,可是在其余范畴中可能就不这么清晰了。
2. 任一对象 A,都有一个箭头,它是复合的最小单位。这个箭头从对象出发又指向对象自身。做为复合的最小单位,意思是当它分别与任何从 A 开始或终止于 A 的箭头复合时,获得的依然是与后者相同的箭头。对象 A 的单位箭头称为 idA,意思是 identity on A,即 A 与自身恒等。在数学表示中,若是 f 从 A 到 B,那么就有
f∘idA = f
以及
idB∘f = f
在处理函数时,恒等箭头就是做为一个恒等函数实现的,这个函数的惟一工做就是直接返回它所接受的参数值。对于全部的类型,均可以这么实现恒等,这意味着这个函数是多态的。在 C++ 中,咱们能够以模板的形式来定义它:
template<class T> T id(T x) { return x; }
固然,在 C++ 中,实际状况并不是如此简单,由于你须要考虑要给这个函数传递什么以及如何传递(经过值,仍是经过引用,仍是经过常量引用,仍是经过 move 语义等等)。
在 Haskell 中,恒等函数是标准库(即 Prelude)中的一部分,其定义以下:
id :: a -> a id x = x
正如你所见,在 Haskell 中多态函数是小菜一碟,在其声明中,你只须要用一个具体的类型来替换掉类型变量便可。这就涉及到一个小把戏:具体的类型,名字老是以一个大写字母开头,而类型变量的名字老是以一个小写字母开头。在此,a
表示全部类型。
Haskell 函数的定义由尾随着形参的函数名构成,这里只有一个形参 x
。函数体在 =
号以后。这种简洁扼要的风格常常令新手愕然,但你很快就会发现它的魅力所在。函数的定义与调用是函数式编程的面包与黄油,所以它们的语法被简化到了骨瘦如柴的境地。参数值列表不只不须要括号,参数值之间也没有逗号(下文在定义多个参数的函数时,就能够看到这些)。
函数体老是由一个表达式构成,亦即函数中没有语句。一个函数的返回结果就是这个表达式自己——在此就是 x
。
这就是咱们的 Haskell 第二课。
恒等条件可写为(仍是伪 Haskell 代码):
f . id == f id . f == f
可能你会问:为什么须要这个什么也不作的恒等函数?其实你应该这样问,为何须要数字 0?
0 是一个表示无的符号。古罗马人有一个没有 0 的数字系统,他们可以修建出色的道路与水渠,有些直到今天还能用。
相似 0 这样的东西,在处理符号变量的时候特别有用。这就是罗马人不擅长代数学的缘由,而阿拉伯人与波斯人由于熟悉 0 的概念,所以他们可以很好的掌握代数学。当恒等函数做为高阶函数的参数值或返回值时,它的价值就会得以体现。高阶函数可以像处理符号那样处理函数,它们是函数的代数。
总结一下:一个范畴由对象与箭头(态射)构成。箭头能够复合,这种复合知足结合律。每一个对象都有一个恒等箭头,它是箭头复合的基本单位。
函数式程序员在洞察问题方面会遵循一个奇特的路线。他们首先会问一些似有禅机的问题。例如,在设计一个交互式程序时,他们会问:什么是交互?在实现基于元胞自动机的生命游戏时,他们可能又去沉思生命的意义。秉持这种精神,我将要问:什么是编程?在最基本的层面,编程就是告诉计算机去作什么,例如『从内存地址 x 处获取内容,而后将它与寄存器 EAX 中的内容相加』。可是即便咱们使用汇编语言去编程,咱们向计算机提供的指令也是某种有意义的表达式。假设咱们正在解一个难题(若是它不难,就不必用计算机了),那么咱们是如何求解问题的?咱们把大问题分解为更小的问题。若是更小的问题仍是仍是很大,咱们再继续进行分解,以此类推。最后,咱们写出求解这些小问题的代码,而后就出现了编程的本质:我么将这些代码片断复合起来,从而产生大问题的解。若是咱们不能将代码片断整合起来并还原回去,那么问题的分解就毫无心义。
层次化分解与从新复合的过程,并不是是受计算机的限制而产生,它反映的是人类思惟的局限性。咱们的大脑一次只能处理不多的概念。生物学中被广为引用的一篇论文指出咱们咱们的大脑中只能保存 7± 2 个信息块。咱们对人类短时间记忆的认识可能会有变化,可是能够确定的是它是有限的。底线就是咱们不能处理一大堆乱糟糟的对象或像兰州拉面似的代码。咱们须要结构化并不是是由于结构化的程序看上去有多么美好,而是咱们的大脑没法有效的处理非结构化的东西。咱们常常说一些代码片断是优雅的或美观的,实际上那只意味着它们更容易被人类有限的思惟所处理。优雅的代码创造出尺度合理的代码块,它正好与咱们的『心智消化系统』可以吸取的数量相符。
那么,对于程序的复合而言,正确的代码块是怎样的?它们的表面积必需要比它们的体积增加的更为缓慢。我喜欢这个比喻,由于几何对象的表面积是以尺寸的平方的速度增加的,而体积是以尺寸的立方的速度增加的,所以表面积的增加速度小于体积。代码块的表面积是是咱们复合代码块时所须要的信息。代码块的体积是咱们为了实现它们所须要的信息。一旦代码块的实现过程结束,咱们就能够忘掉它的实现细节,只关心它与其余代码块的相互影响。在面向对象编程中,类或接口的声明就是表面。在函数式编程中,函数的声明就是表面。我把事情简化了一些,可是要点就是这些。
在积极阻碍咱们探视对象的内部方面,范畴论具备非凡的意义。范畴论中的一个对象,像一个星云。对于它,你所知的只是它与其余对象之间的关系,亦即它与其余对象相链接的箭头。这就是 Internet 搜索引擎对网站进行排名时所用的策略,它只分析输入与输出的连接(除非它受欺骗)。在面向对象编程中,一个理想的对象应该是只暴露它的抽象接口(纯表面,无体积),其方法则扮演箭头的角色。若是为了理解一个对象如何与其余对象进行复合,当你发现不得不深刻挖掘对象的实现之时,此时你所用的编程范式的本来优点就荡然无存了。
用你最喜欢的语言(若是你最喜欢的是 Haskell,那么用你第二喜欢的语言)尽力实现一个恒等函数。
用你最喜欢的语言实现函数的复合,它接受两个函数做为参数值,返回一个它们的复合函数。
写一个程序,测试你写的能够复合函数的函数是否能支持恒等函数。
互联网是范畴吗?连接是态射吗?
脸书是一个以人为对象,以朋友关系为态射的范畴吗?
一个有向图,在什么状况下是一个范畴?
下一篇 -> 类型与函数