最近比较巧合的接触了Functional programming,经常接触到Monad的概念(在Haskell和F#中),然而除了知道这个概念很难明白以外其他都不太清楚,粗略地看了几篇相关文章讲的很晦涩难懂。所以决定花一段时间将这个FP的奇怪概念搞个明白。
什么是monad?
太长了,懒得看
恩我也是这么想的。。所以我一直没看,我在这里尝试着来用简单一些的文字说明一下Monad的概念,有可能每段都有错误,谨慎啊。
Monad是一个FP中的专有名词,是一个含有变量的类,monad是Monad这个类的实例。这个类的作用是把一系列操作连接在一起。没错你可能想到do关键字,比如最简单的IO实例:
do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Welcome, " ++ name ++ "!")
在这里do之后的list里面我们做了三件事情,打印,读取IO输入,输出结果。在这三句话中间传递了参数name,这是一个state,这个state存在的意义是使得我们能够以pure functional的方式执行这三句话。
然而do关键字是一个语法糖,在这个语法糖的背后是一个>>=
,也就是bind操作符在支持这个操作的连续性。那么在这里说,Monad就是一个支持>>=
操作符的类,就是一个能够连接多个operation的类。没错刚才那段代码就是一个monad,就是一段由几个function组成的control flow,bind操作符的作用就是读取左边操作的结果并且让右边操作能够使用。很简单
这说的太简单了,肯定是错的
是说的很简单,但是不能说是错的吧。。举个例子,假如有个没有概念的人问你“什么是函数”,怎么回答?一段映射?读取几个参数输出几个参数的代码?这么简单的概念你怎么使别人相信函数在编程过程中的重要作用呢,很难吧。Java中我们当然可以写一个函数,读取一个int返回一个int,这严格的遵守了数学函数的定义,但是同时这个函数还可以做很多其他的事情,比如打印出来,比如进一个死循环,但是这些不是pure function能做的,haskell这样的语言中function就应该读取一个值返回一个值,它对程序的影响只能体现在它的返回值上。“什么是函数”这个问题的答案大概长什么样子我想大家心里应该有数。
Monad能发挥巨大作用,不是因为它的定义太复杂,是因为他不只是简单的定义,而是可以延伸出无数个种类。没错bind操作符的确就是简单地把参数从左边传给右边,能包含bind操作符的都是monad,但是monad还可以同时做很多其他的事情,做的事情不一样monad的作用也不一样。换句话说,不同monad赋予了>>=
不同的意义。
举一个不是我想出来的例子,以下代码是javascript的几个函数。
var sine = function(x) { return Math.sin(x) };
var cube = function(x) { return x * x * x };
var sineCubed = cube(sine(x));
sineCubed是一个组合函数,可以很简单的把它在Haskell中写出来。然而这个时候我们队sineCubed有个特殊的要求,要求它能够打印出来自己运行时的值。javascript中间在函数里加一句console.log即可,但是Haskell中间呢?没办法在sine:: Number -> Number
这个函数中间加一句打印,那样违反了pure function的原则。那么我们只能在返回值中体现出来:
var sine = function(x) {
return [Math.sin(x), 'sine was called.'];
};
var cube = function(x) {
return [x * x * x, 'cube was called.'];
};
那么sineCubed此时应该怎么写?假设还是之前的写法,那么我们会发现cube函数需要读取一个数组了,无法执行。这时候就需要多做一步处理,假设这个时候我在做一个作业,只要完成作业即可,那我可能就是sineCubed中cube只读取sine返回值的第一个参数,或者改变cube的签名为cube :: (Number,String) -> (Number,String)
野蛮地完成任务(语言穿越了,只是为了表达意思)。这显然不是best practice,更优雅的方法是什么?没错这位同学答对了,就是对参数进行一个包装和解包装的工作,我们需要一个工具能够做这个事情。
首先需要一个unit,它读取一个Number,将这个number放在一个container里,返回一个(Number,String)。
// unit :: Number -> (Number,String)
var unit = function(x) { return [x, ''] };
unit函数使得原本简单的返回值可以被包装成包含了其他信息的值。然后在lift函数中我们用到了unit:
// lift :: (Number -> Number) -> (Number -> (Number,String))
var lift = function(f) {
return function(x) {
return unit(f(x));
};
};
lift的签名是读取一个函数,返回一个函数,它将一个简单函数"lift"到了一个包含了一个其他信息的函数。要做sineCubed,我们还需要一个函数能够组合几个函数,这是compose做的事情,它简单地把两个函数复合起来。可以想象还需要一个bind,它使得原本的sine和cube的函数签名能够被修改成想要的类型。这几个抽象的函数概念就组成了一个monad,实际上bind和unit就组成了一个monad,刚刚做的事情其实就是Haskell的Writer
monad所做的事情。打开这个链接看看,应该你就能懂了。
这么说,monad其实是一种design pattern?
我个人觉得拿javascript去形容Haskell中的概念是一件容易误导人的事情,之前在一篇很长的blog中也看到说“不要用其他语言的思维去考虑函数式语言”。说monad是一种design pattern是有一定道理的,假设你在程序中需要一个函数接受一类输入,得到另外一类的输出,那么就要考虑用到bind和unit这样的函数,unit函数包装参数的类得到需要的另外一种类型,bind函数修改原函数使得它能够接受自己返回的函数类型。这样做的好处是可以在达到目的的同时,避免对原来的代码做出“毁灭性”的彻底修改。
然而之上说的只是一种monad类型,并不适用到全部范围。我们来看看其它几种monad:
-
在一系列操作中,每一步都返回一个success/failure的标志,只有success才执行下一步,failure则自动终止。这是Failure Monad。
-
将返回标志改成Exception的处理,这是Error Monad, Exception Monad,如何处理完全可以自定义。
-
每一步返回多个结果,在下一步遍历这些结果,进行筛选或者处理,这是List Monad。
-
每一步操作都是针对state的一个action,下一步操作只从上一步操作返回的world status得到信息进行操作,bind操作使得IO的side effects能够保证按顺序处理,这是I/O Monad。(这里说的太笼统,最好再看看I/O Monad的说明。。)
更合适的说,Monad是一个通用的将各个函数作为组建搭建起来的“积木”,这个积木有两个基本部件"return"和">>=",而且这两个部件满足一些特定的组合性质,那么我就可以说我搭建的是一个monad:
- (return x) >>= f == f x
- m >>= return == m
- (m >>= f) >>= g == m >>= (\x -> f x >>= g)
orz...
这些事情,不用Monad当然也能做到,但是Monad的意义是使得达到这些目的的方法简单很多,只需要去定义>>=
做什么事情即可达到目的。
打字好累。。stackoverflow的这个链接有很多大牛讲解了自己对Monad的理解,看完这篇日志之后再去看这个链接可能会稍微多理解一些东西(或者可以发现我说错了那麻烦告诉我一下=,=)
第一次在简书上写日志,markdown的设置弄了很久。。希望这篇日志能够对大家有点小帮助
网友评论
我能这样理解么?通过 func1(x) { func2(x) }的方式嵌套函数,这是函数式编程思维。
遵循monad的函数 通过 定义>>=的操作符,可以解决函数嵌套的参数包装和传递问题。
如果理解错误请指正,感谢。