函数式编程

什么是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) 。
  (组合箭头和元箭头映射这里省略)

  函子这种映射实际是一种分解组合方式,对于这个过程我们可以用下面模拟形象地理解:

  1. 计算C集合中每个函数的"结果", 但是不组合它们.
  2. 将 F函数单独应用于C中每个函数的结果,我们就获得结果的集合的集合。
  3. 压平这两层集合,组合所有的结果。 (注意这里的组合方式将对应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,特殊在哪里呢?就是自己对自己进行转换的集合而已。

 

函数编程中functor和monad的形象解释

什么是Monoid?

Javascript的函数式编程术语解释

为什么需要Monad?

用monad替代嵌套回调

范畴category:组合的本质

加法是自然之道 

Scala使用Reader Monad实现依赖注入

monad专题