函数式编程
什么是Monad?
为了理解什么是Monad,最好需要了解什么是Monoid。这两篇互为姐妹篇,因为Monad的定义是:A monad is just a monoid in the category of endofunctors, what's the problem?
what's the problem?其实问题大了去了,这个定义中首先我们知道monad是一个特殊的monoid,特殊在哪里?是在endofunctor这个范畴里,正如书桌比桌子特殊,特殊在书桌用在看书读书这个范围里。所以,我们需要首先了解什么是endofunctor,而endofunctor是end of functor的组合,还得首先了解什么是functor函子。
自函子endofunctor
一个functor是一个多态数据类型,支持用函数对数据内容的操作,这非常类似面向对象概念中的对象,一个对象也是由数据和方法组成,方法是操作数据的函数。所以,当我们认为世界上每个都是对象时,也可以认为每个都是functor。
一个functor是将一个范畴(广群)转换到另外一个范畴(广群),而endofunctor则是转换的源范畴和目标范畴是同一个,endofunctor是functor的特殊。
我们知道:一个范畴是由三个部分组成: 元素对象、态射(又称为箭头)以及二元运算。如果这个范畴又满足结合律,那么它是一个半群,如果又满足幺元,那么是幺半群,也就是Monoid。
因此,functor实际是将一个Monoid中的元素对象映射到另外一个Monoid的元素对象,态射也是这么映射。而自函子endofunctor映射的这两个Monoid是同一个。
在Haskell中,一个endofunctor被称为"the Hask category,",是Hask范畴。Hask范畴中的元素对象是Haskell的类型,态射箭头是Haskell的函数。
类型
类型是值的集合,比如类型Bool(Haskell核心类型是大写开始)是一个有两个元素的集合,这两个元素 是True和False,类型Char是所有Unicode字符如'a'或'b'的集合。
函数也是有类型的,函数的类型是其输入输出的综合,比如f函数是输入类型A到输出类型B:B f(A a); 在 Haskell中是如下表达:
f ∷ A → B
因此区别两个函数的不同,我们可以从其输入类型和输出类型上区别。
函子functor是比函数更高阶的函数,函子是作用于两个范畴之间的函数,但是根本上也是一个函数,因此函子的类型与上面的函数类型差不多。假设两个范畴是 C和D, 其函函子是:
functor F: C -> D
函子functor原理
函数组合的方式有其特殊地方,这个特殊主要是由于我们组合的对象是函数,如果组合的对象是整数类型,两个整数组合成一个整数,这没有问题,但是你不能将两个函数类型组合起来还是和原来函数类型一样。比如我们将两个f函数f ∷ A → B组合起来,就不会得到还是A → B。
函子functor是比函数更高阶的函数,函子是作用于两个范畴之间的函数,可以简单认为是两个集合之间的映射。范畴的映射转换需要转换其中的元素和态射。
假设两个范畴是 C和D, 有一个函子functor F: C -> D ,这种写法类似函数写法,但是因为函子是范畴的函数,所以,其工作原理是进入范畴C和D内部,而范畴是由元素对象和态射箭头组成,因此函子就要分别作用于元素对象和态射箭头。
映射元素对象:C中的任何对象A转变成了D中的F(A);
映射态射箭头:C中的态射f: A -> B转变成了D中的F(f): F(A) -> F(B) 。
(组合箭头和元箭头映射这里省略)
函子这种映射实际是一种分解组合方式,对于这个过程我们可以用下面模拟形象地理解:
- 计算C集合中每个函数的"结果", 但是不组合它们.
- 将 F函数单独应用于C中每个函数的结果,我们就获得结果的集合的集合。
- 压平这两层集合,组合所有的结果。 (注意这里的组合方式将对应Monad的自然变换态射)
可以用Java的集合操作来形象理解 :
Collection ednfunctor(Collection c){
Collection d = new ArrayList();
for(Object o:c) {
//对输入集合C元素逐个应用函数F转换到新的输出集合
}
return d;
}
这种转换一般是由SDK专门工具来提供,如果自己实现就非常丑陋,
Monad
现在回到Monad英文定义:
A monad is just a monoid in the category of endofunctors
the category of endofunctors是自函子endofunctor的范畴(category),Monad是自函子范畴中的Monoid。
前面我们已经了解函子的映射原理,自函子可以理解为映射范畴C到另外一个范畴C。那么自函子的范畴是什么意思?是基于自函子的新范畴吗?经过查询此句原出处: Here it is in context,原文是:
All told, a monad in X is just a monoid in the category of endofunctors of X, with product × replaced by composition of endofunctors and unit set by the identity endofunctor.
确实是基于范畴X的新范畴,这个新范畴看成是endofunctors on X。范畴X之上的范畴,这个新范畴的元素对象是自函子endofunctor,谈到范畴就要想到元素对象和态射,那么其态射是什么呢?
前面我们解释函子funtor工作原理时,有一句:"3.压平这两层集合,组合所有的结果",这个组合方式是一种自然变换natural transformation,属于新范畴的态射,因为态射有份两种:组合态射和元identitiy,自然变化就要对分别对X范畴中这两种进行转换。
所以,Monad工作原理包含两个部分:对原范畴组合成新的范畴,这个范畴对于Monad来说必须是幺半群Monoid,可以认为Monad是一系列自函子的组合,这种组合是一种转换,转换的结果是Monoid。
Monad有以下特征:
- Monad是一种定义将函数(函子)组合起来的结构方式。(monoid是定义元素对象组合起来的结构方式。如果元素对象是特殊种类:函数(函子),那么它可能是Monad)
- 这些组合的方法都是符合结合律的
- 有一个特殊幺元,能够和任何元素组合,导致的结果是不改变这些元素。
关键对最后一点幺元讲解一下,以为什么需要Monad?一文中案例为说明,假设有两个数字a和b相加,这里a和b 可能为空,Java 代码如下:
int try_to_add_numbers( Integer a, Integer b )
{
return a + b;
}
如果a 和b非空,那么这个方法将会返回它们的总数,但是如果其中有一个是空的,我们会得到NullPointerException错误,调用客户端得到这个错误必须去处理它。而函数的定义是有一个输入类型和一个输出类型,现在又跑出第三种类型Exception,很显然Exception是和输入输出类型不属于同一个范畴,这就不符合封闭运算了。
那么我们使用一个幺元,比如Optional来封装结果,这样就能保证不抛出Exception,而是将Exception错误通过输出结构输出,这种结果分两种,要么是空,要么是有值,如果是有值,打开它就能获得真正的计算结果。我们使用Optional与结果值结合,但是不会改变这个结果值类型。具体可见: Java8中option实现Monad
总体来说:Monoid是元素对象的组合的范畴,如果这种元素对象是函数或函子(也可能是Pipe,这就复杂了去了 ),那么Monad是自函子的组合范畴,Monad也是一种特殊的Monoid子集。
如果你对大数据Hadoop等比较熟悉,map/reduce其实也是一个Monad。
最后我们用简单大白话(不精确有助于理解)翻译一下Monad的英文定义:A monad is just a monoid in the category of endofunctors,monad只不过也是一种特殊情况下的monoid,特殊在哪里呢?就是自己对自己进行转换的集合而已。