范畴论要解释 Monad,就必须提到范畴论(Category Theory)。范畴(category)本身是一个很简单的概念。一个范畴由对象(object)以及对象之间的箭头(arrow)组成。范畴的核心是组合,体现在箭头的组合性上。如果从对象 A 到对象 B 有一个箭头,从对象 B 到对象 C 也有一个箭头,那么必然有一个从对象 A 到对象 C 的箭头。从 A 到 C 的这个箭头,就是 A 到 B 的箭头和 B 到 C 的箭头的组合。这种组合的必然存在性,是范畴的核心特征。以专业术语来说,箭头被称为态射(morphisms)。范畴中对象和箭头的概念可以很容易地映射到函数中。类型可以作为范畴中的对象,把函数看成是箭头。如果有一个函数 f 的参数类型是 A,返回值类型是 B,那么这个函数是从 A 到 B 的态射;另外一个函数 g 的参数类型是 B,返回值类型是 C,这个函数是从 B 到 C 的态射。可以把 f 和 g 组合起来,得到一个新的从类型 A 到类型 C 的函数,记为 g ∘f,也就是从 A 到 C 的态射。这种函数的组合方式是必然存在的。
一个范畴中的组合需要满足两个条件:
- 组合必须是传递的(associative)。如果有 3 个态射 f、g 和 h 可以按照 h∘g∘f 的顺序组合,那么不管是 g 和 h 先组合,还是 f 和 g 先组合,所产生的结果都是一样的。
- 对于每个对象 A,都有一个作为组合基本单元的箭头。这个箭头的起始和终止都是该对象 A 本身。当该箭头与从对象 A 起始或结束的其他箭头组合时,得到的结果是原始的箭头。以函数的概念来说,这个函数称为恒等函数(identity function)。在 Java 中,这个函数由 Function.identity() 表示。
从编程的角度来说,范畴论的概念要求在设计时应该考虑对象的接口,而不是具体的实现。范畴论中的对象非常的抽象,没有关于对象的任何定义。我们只知道对象上的箭头,而对于对象本身则一无所知。对象实际上是由它们之间的相互组合关系来定义的。
范畴的概念虽然抽象,实际上也很容易找到现实的例子。最直接的例子是从有向图中创建出范畴。对于有向图中的每个节点,首先添加一个从当前节点到自身的箭头。然后对于每两条首尾相接的边,添加一条新的箭头连接起始和结束节点。如此反复,就得到了一个范畴。
范畴中的对象和态射的概念很抽象。从编程的角度来说,我们可以找到更好的表达方式。在程序中,讨论单个的对象实例并没有意义,更重要的是对象的类型。在各种编程语言中,我们已经认识了很多类型,包括 int、long、double 和 char 等。类型可以看成是值的集合。比如 bool 类型就只有两个值 true 和 false,int 类型包含所有的整数。类型的值可以是有限的,也可以是无限的。比如 String 类型的值是无限的。编程语言中的函数其实是从类型到类型的映射。对于参数超过 1 个的函数,总是可以使用柯里化来转换为只有一个参数的函数。
类型和函数可以分别与范畴中的对象和态射相对应。范畴中的对象是类型,而态射则是函数。类型的作用在于限定了范畴中态射可以组合的方式,也就是函数的组合方式。只有一个函数的返回值类型与另一个函数的参数类型匹配时,这两个函数才能并肯定可以组合。这也就满足了范畴的定义。
之前讨论的函数都是纯函数,不含任何副作用。而在实际的编程中,是离不开副作用的。纯函数适合于描述计算,但是没办法描述输出字符串到控制台或是写数据到文件这样的副作用。Monad 的作用正是解决了如何描述副作用的问题。实际上,纯粹的函数式编程语言 Haskell 正是用 Monad 来处理描述 IO 等基于副作用的操作。在介绍 Monad 之前,需要先说明 Functor。 |