为什么需要Monad?

banq 14-11-29
                   

这篇Motivation for Monads其实谈了为什么需要函数编程和Monad?指出函数编程的几个特点,与面向过程编程的本质区别,如不能抛出Exception等,从而导出为什么使用Monad的动机原因。

这不是一篇monad教程,介绍monad的文章已经有很多了,这些文章只是解释了什么是Monad以及Monad是如何工作的(What和How),这里只想解释为什么使用Monad?(Why)

为了解释为什么需要Monad,首先我们需要解释为什么使用函数风格编程?这有两个广泛认可的特征和一个争议点:

1.函数式第一等公民且是高阶的。函数能在其他函数中声明;函数可以被传递,可以像值一样存储在数据结构中。

2.隐式效果强烈不鼓励。

3.可选fancy的类型系统是重要的 (这是有争议的)

主要我们看看第二点,为什么不能使用隐式效果?函数风格的函数其实是数学函数,这意味著一个函数必须有输入和输出,这是与很多面向过程编程非常不同的地方,传统过程式编程中, 函数/过程/方法是可以允许和它们的环境交互的,导致没有直接输入参数或返回值,也就是说一个函数可以没有输入或没有输出。

比如:
1. 随机数的产生,在需要随机数产生库包中,有一个过程方法称为nextNumber,这是一个无方法参数但是返回一个数字的方法,其实在其背后,它访问了数字产生器的状态,但是这一点都没有在其接口中表露出来。

2.索引,索引结构如vector通常有一个get方法,这是从vector获得某个数值的方法,它是返回指定索引位置的值,但是如果索引大小范围超过了vector整个有效长度,它就会抛出一个Exception。

3.系统I/O, 许多过程性方法与系统交互,比如读写文件等等,发送和接受消息。

在一个纯函数风格编程中这些副作用是被杜绝的,那么是不是说函数语言不能做这些事情呢?当然不是,这些效果必须显示地表现在函数的输入和输出之中。

1.随机数产生器是一个简单案例,一个函数风格的随机数产生器会将当前产生器的状态作为输入,而返回一个产生的数字和产生器下一个状态的综合结果,客户端调用代码有责任将产生器状态从一个函数调用线性传递到下一个。

2.Exception不允许在函数风格代码中,这样我们得显式明确编码出错然后返回值,如haskell的代数数据类型Maybe和ML选项非常方便适合做这些事情,但是不是必须的。

3. I/O 是广泛的,我们如何使用函数风格与世界交互呢?通过发明一个不透明opaque 的对象,代表世界的状态,概念上你可以想象处理I/O的函数是将世界令牌token传递到下一个需要I/O处理的函数中。

为什么??
许多程序员并不真的知道函数风格,这里不再罗嗦,互联网上有许多资料,这里指出一个使用函数风格的理由是:测试。

任何人编程复杂程序都需要测试,测试必须设置状态和上下文场景以便能找到Bug,这是非常耗时的,以函数风格编程意味着一切都是函数依赖你传入的参数,没有隐藏的上下文依赖,它使得设置测试更加容易。

为什么需要Monad?
前面谈了为什么需要函数编程,现在是Monad,正如你在前面例子看到,从面向过程风格切换到函数风格意味更多工作是在函数的输入与输出上,有时会带来大量工作,这是非常痛苦的,这意味着在过程化编程中所有隐藏的状态和顺序效果都要显露出来,只有函数编程的铁杆粉丝才认为暴露这些隐藏的管道风格无疑是一个好事。

Monad的存在就是将这些暴露的管道重新埋回去,这样我们的代码看上去更像我们起步学习的传统的过程编程,但是有本质区别,显式的状态线索thread递交还是会发生的,只是比纯粹暴露管道的方式更方便漂亮了。

exception与显式错误返回是一个好的例子,让我们假设我们需要一个函数是两个数字相加,除非我们不能确定这两个数字是否为空,也就是是否存在,如果存在就相加。Java代码:


int try_to_add_numbers( Integer a, Integer b )
{
return a + b;
}


如果a 和b非空,那么这个方法将会返回它们的总数,但是如果其中有一个是空的,我们会得到NullPointerException错误,调用客户端得到这个错误必须去处理它。

在管道暴露风格的函数风格中,我们可以如下编写(OCaml语法)


let try_to_add_numbers( a, b ) =
match ( a, b ) with
( Some an, Some bn ) -> Some ( an + bn )
| _ -> None

这个代码会显式地检查两个参数是否不为空non-null,并且显式地返回一个或者是None或者是包裹着Some的结果,并没有任何错误抛出和情况处理。

在Haskell中,我们写:


try_to_add_numbers a b = liftM2 (+) a b

其中+函数希望只能得到真正的数字,liftM2确实神奇,它是一种高阶函数,它会将获取一个带有两个参数的函数然后返回一个新的函数,新的函数也是带有两个参数,但是已经插入了一些管道,这些管道作用是,如果输入是Nothing(Haskell是null),整个函数将是Nothing,如果两个都有数字值,那么它会计算出它们的总数,并及时包装这个结果(Haskell是non-null)(banq注: Java8中是option实现Monad)

最后,解释一下为什么会在动态函数风格倡导者(如Lisp)和fancy可选类型的函数风格(如ML)之间有分歧呢?

liftM2的类型和它的近亲是相当复杂的。 如果一个人 想做这种编程,比如说Java,很有可能 您必须编写的类型将是令人恐惧地冗长且拜占庭式荣华。 而fancy可选类型的语言如(Haskell,Scala OCaml)则用类型推断减轻这种问题,这使得它 可以省去大部分类型注解。

另外一方面,动态语言会完全避免复杂类型问题,但是却完全丢失了类型。(banq注:为什么Javascript需要类型?)

如果你想将编程风格从隐式效果转化为显式的函数传递,复杂的高阶函数是必要的,这会保持你的代码相当干净。 而复杂的高阶函数往往有fancy可选的类型。

[该贴被banq于2014-11-29 13:43修改过]

                   

3
banq
2014-11-29 18:56

根据这篇文章触发的灵感,写了两篇定义性文章:

什么是Monoid?

什么是Monad?

一般网上谈到这两个概念,总是以Haskell或Scala为案例,由于对具体代码的熟悉程度不同,致使人们理解一个复杂概念的同时又引入新的陌生概念,个人认为这不是一种好的学习或分享方式,因此在这两篇中尽量如何本文楼上的原作者一样,试图把看似高深的范畴论用简单的大白话解释一下,范畴其实就是一个集合,只不过对这个集合有一些约束定义,如果熟悉业务建模的人可能对这种思维方式不陌生,经常我们会对抽象的事物进行定义,数学上基本就是用集合概念,实际上从罗素悖论拯救了数学开始,数学离不开集合,对于一个国家社会,个人与社会也是不同的概念,社会是一个集合概念,从集合角度考虑问题和从个人元素角度考虑问题是两种截然不同的思路,如果你平时注重这种区别,你可能会有两种人生观。祝你脑洞大开。