之前说了函数式编程的收获。比如说函数可以当作变量,然后尽量避免写副作用的程序。
之后可以说遇到了一个超级难理解的东西–monad。
当我在写java时,大概是下面的一段代码
List.map( item -> item.getName());
List.flatmap( item -> item.getName()); // ??
然后不知道map与flatmap的区别。于是对于一个懒人程序猿来说,答案当然不是去问谷歌,而是拉来一个同事。
本人: “小田君,flat和flatmap有什么区别?“
小田: “啊,这个是monad啊“
本人: “莫纳多?”
小田: “嗯,monad”
本人: “莫纳多是什么玩意儿?”
小田: “这个和范畴论有关哦”
本人: “那(tmd)的范畴论又是什么啦?”
小田: “范畴论其实我也不是很懂,不过map和flatmap我觉得可以解释清楚。”
本人: “啊,这样啊,能跟我讲讲吗?(那你喵了个咪的提范畴论干嘛?显得你很浮夸吗?)”
之后小田君用了大概近10分钟跟我讲了一遍两者区别,本人基本上就是“啊~“,“噢!“,“哦?“之类的反应。当时觉得自己听明白了,然后过了两天就忘了,现在想起来他大概讲的还是错的!!!
不过,当小田君教我这些知识时,我真的感觉到我好像和他完全不在一个等级上,顿时感觉自己非常的落伍,得赶紧恶补一下知识。
于是我现实谷歌了monad。然后维基百科了一下,看到的是类似于这种东西。
反正基本觉得这讲的不是人话。
于是就问了度娘,然后看了一些文章,里头出现了一些感念如”单子”之类的。可能是受面向对象思想的影响过深,和自己的耐心太差,根本无法理解里边的内容。于是想,是不是该学一学函数式语言了。
于是看了一本《functional programming in javascript》。因为js基本还会写,学习成本会比较低。意料之中书里有专门的一章讲monad,不过当我看到monad那一章时,由于经过了一段时间,自己之前通过调查对monad的一些理解基本荡然无存,最终出乎意料地没能理解monad。
于是一怒之下之下打开youtube,开搜monad!然后出现了这个老头的视频。
这个叫布莱恩贝克汉姆的老头,不用任何数学专用词汇,很简洁地(至少我看视频时觉得)解释了monad,然后说实话,我没听明白…
于是最终还是决定学一下Haskell,觉得可能用这个语言更容易理解monad。虽说monad这个概念肯定是不依赖于某个语言的。但是语言其实是能帮助理解的,因为你其实是在用语言在思考。
Haskell后发现对布莱恩贝克汉姆的解释容易理解了,不过自己好事处于”这是什么玩意儿,不过反正它好神奇”的状态。
对于自己现在函数式编程的思想还并不像面向对象那样深入骨髓,可能能更好地以”前函数式编程时代”的头脑来说事情。想必很多未接触过函数式编程的人理解monad也会废力一些。尽量不用函数式的专业术语,试着解释一下monad。
网上比较多的说法有两种,名词时自己想的,不是准确的术语。
- 容器论
- 链条论
简单说一下两者的解释
monad像一个容器,容器里存放着一个值。
从国外的网站盗的图,很形象的说明。
2这个数字被放在一个容器里。
你可能会问,首先我们为什么需要一个容器?之后会说的啦…
虽然容器存放着数字2,但容器本身不能直接进行普通的数学运算比如+ 3。容器([2])和3不是一个类型。
那要对容器里的2进行运算该怎么办呢?那就把2从容器中拿出来(如图),但是运算好之后又必须重新放进一个容器(不是2之前用的容器),或者说重新打个包。
但我们好不容易把2从容器里拿出来进行了运算,还要把它重新打包?这有什么意义?反正我刚看到的时候是这么想的。正好可以引向链条论
之前的一篇文章说了,在函数式语言中,函数可以当作变量传来传去,还可以组合。当你把函数组合起来的时候,你的处理就像一条链条一样能连一起。
再写一下伪代码
假设a -> a,表示一个函数。它获得一个a类型的参数,返回一个类型的返回值。
func1 = a -> a
func2 = a -> a
func3 = func1 $ func2 // $表示调用函数。func3也是一种a -> a的函数。
数学上我们经常会写这种运算吧。
2 * 3 + 2 - 7 = ?
四则运算都是接受数字返回数字的函数。我们把*3, +2, -7都看成函数 multiply3, plus2, minus7,然后函数式语言里会是这样
multiply3 & plus2 & minus7 2
回到之前提到的容器,把容器记做M。
假设有个函数接受int, 返回M[int],记做int -> M[int]。那我们可以这种类型的函数给串起来。
func1 = int -> M[int]
func2 = int -> M[int]
func3 = func1 $ func2 // $表示调用函数。func3也是一种int -> M[int]的函数。
嗯?这是不是作弊?int -> int的函数能串起来不奇怪,int -> M[int]怎么串?第一个函数的返回值和第二个函数的参数不一样啊?
所以如果光用容器论来解释这个问题就会比较难懂。我的见解就是Monad还包含了一个行为,M[int]定义了如何把里边的int取出然后扔给后一个int -> M[int]的函数。
这是不是很抽象。举个例子。java中有Optional这个类吧。
/**
* ex: "Michael Fu" -> "MICHAEL"
* @param maybeName
*/
public void givenNameInUpperCase(Optional maybeName){
Optional mayGivenNameInUpperCase = maybeName.flatMap(name -> Optional.of(name.substring(0, name.indexOf(" "))))
.flatMap(name -> Optional.of(name.toUpperCase()));
}
上面的代码用flatMap把两个String -> Optional的函数串起来了。flatMap会负责把Optional中的String解包,然后把String作为参数扔给函数处理。flatMap这个行为是由Optional定义的。
为什么除了a -> a之外我们还需要a -> M[a]。而且是M[a]是来解决具体问题的。
从上面的代码例子,我们可以直接地体会到,如果没有Optional这个东西,我们的处理会有这样的语句
if(str == null){}
但是人们会很在意个一个if语句吗?但是写函数式语言时,会尽量写成链条的样子,而且写太多if容易编程命令式编程(imperative programming)的风格。你是不是曾经写过类似下面的代码很多遍?
List numbersGreaterThan3 = new List<>()
for(int num : nums){
if(num > 3) {
numbersGreaterThan3.add(num);
}
}
而如果用Stream的话,就很简洁啦。
List numbersGreaterThan3 = nums.stream().filter(num -> num > 3).collect(Collectors.toList());
还有另外一个比较典型的例子就是Promise。如果你写过js,你可能掉入过回调地狱(callback hell)。
const verifyUser = function(username, password, callback){
dataBase.verifyUser(username, password, (error, userInfo) => {
if (error) {
callback(error)
}else{
dataBase.getRoles(username, (error, roles) => {
if (error){
callback(error)
}else {
dataBase.logAccess(username, (error) => {
if (error){
callback(error);
}else{
callback(null, userInfo, roles);
}
})
}
})
}
})
};
一个解决方案便是promise(现在还有加强版的aysnc await),然后代码就能写成链式的了。
const verifyUser = function(username, password) {
database.verifyUser(username, password)
.then(userInfo => dataBase.getRoles(userInfo))
.then(rolesInfo => dataBase.logAccess(rolesInfo))
.then(finalResult => {
//do whatever the 'callback' would do
})
.catch((err) => {
//do whatever the error handler needs
});
};
Optional, Stream, Promise都是利用了这个monad概念。另外容器的解包不是所有monad都一样的。容器会有自己的解包方式,有兴趣大家可以看实现。Optional的话是比较简单的
public Optional flatMap(Function super T, Optional> mapper) {
Objects.requireNonNull(mapper);
if (!isPresent())
return empty();
else {
return Objects.requireNonNull(mapper.apply(value));
}
}
好像这些东西很神奇(至少我第一次理解monad时是这么觉得的),很多的问题都用一种概念或者思想给解决的。但它真的只是为了代码的优美而存在的吗?
前边提到的布莱恩贝克汉姆解释说其实monad是为了限制副作用。不过很遗憾,我们能很好地理解到那个层面。如果用我的话来说的话,monad把不确定的因素从处理的主流程中分开来了,不需要很多的分支,能把处理写成一条链。
比如当你写一个向数据库查询一个人,结果可能有数据活没有,甚至数据库没连上。我们可以这样定义函数。用Optional来表示返回值。当找不到的情况下,我们可以返回Empty。
Optional<Person> findPerson(PersonId personId);
然后获得一个人的姓名的处理就会变成
public Option getPersonName(PersonId personId){
return findPerson(personId).flatMap(person -> Optional.of(person -> person.getName()));
}
而Promise则帮我们回吊函数何时调用的不去定型给排除了。
一直在说到容器的事情。在函数式语言中,放进容器的东西失去不出来的。
Optional<Integer> mayNum = Optional.of(10);
num = mayNum.get(); // <- 从函数式编程的角度,这样做是不好的
这可能违背我们的直觉?取不出来那还有什么用?
而答案必须经由容器来操作容器中的值,因为返回的也是容器,所以一切的操作都在容器内。
那Optional的例子来说感觉上就是
Optional -> Optional -> Optional这样一路下去。
容器帮我们隐蔽了不确定性。但不确定性还是存在的。
比如Optional
中可能有数字也可能没有,所以理所当然它没有办法返回一个确定的数字。所以建议大家在写非纯函数式语言的时候注意这个细节,尽量把所有的处理写在和容器交互的函数内,而不是把容器中的东西拿出来。
我有的时候突然觉得这有点像面向对象编程。对象分装了数据,你不能直接去操作数据,必须通过对象开放的接口来进行处理。
说了一下自己的monad的理解。
- 可以把monad当作一种容器
- monad用来控制副作用(本人尚未理解)
- 放入容器后的东西,无法取出,只有容器才能对它操作,处理后还是以容器的形式返回
不能说有多深入或者独到的理解,只能当作自己学习笔记吧,如果今后对monad有了更好的理解,希望能再写一写。
其实看java的代码还是能知道区别的。
map接受的函数,函数的返回值就是普通的任何类型。 a -> b
flatMap接受的函数,函数的返回值必须是Stream(容器) a -> M[b]
http://www.ruanyifeng.com/blog/2015/07/monad.html
https://blog.hellojs.org/asynchronous-javascript-from-callback-hell-to-async-and-await-9b9ceb63c8e8