固执的程序员学习函数式编程的收获 之 二 说说monad

news/2024/7/3 2:29:21

之前说了函数式编程的收获。比如说函数可以当作变量,然后尽量避免写副作用的程序。
之后可以说遇到了一个超级难理解的东西–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

网上比较多的说法有两种,名词时自己想的,不是准确的术语。
- 容器论
- 链条论
简单说一下两者的解释

容器论

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<String> maybeName){
       Optional<String> 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定义的。

这样的monad有什么意义?

为什么除了a -> a之外我们还需要a -> M[a]。而且是M[a]是来解决具体问题的。
从上面的代码例子,我们可以直接地体会到,如果没有Optional这个东西,我们的处理会有这样的语句

if(str == null){}

但是人们会很在意个一个if语句吗?但是写函数式语言时,会尽量写成链条的样子,而且写太多if容易编程命令式编程(imperative programming)的风格。你是不是曾经写过类似下面的代码很多遍?

List<Integer> numbersGreaterThan3 = new List<>()
for(int num : nums){
  if(num > 3) {
    numbersGreaterThan3.add(num);
  }
}

而如果用Stream的话,就很简洁啦。

 List<Integer> 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<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {
        Objects.requireNonNull(mapper);
        if (!isPresent())
            return empty();
        else {
            return Objects.requireNonNull(mapper.apply(value));
        }
    }

这样的monad有什么意义 * 2

好像这些东西很神奇(至少我第一次理解monad时是这么觉得的),很多的问题都用一种概念或者思想给解决的。但它真的只是为了代码的优美而存在的吗?
前边提到的布莱恩贝克汉姆解释说其实monad是为了限制副作用。不过很遗憾,我们能很好地理解到那个层面。如果用我的话来说的话,monad把不确定的因素从处理的主流程中分开来了,不需要很多的分支,能把处理写成一条链。
比如当你写一个向数据库查询一个人,结果可能有数据活没有,甚至数据库没连上。我们可以这样定义函数。用Optional来表示返回值。当找不到的情况下,我们可以返回Empty。

  Optional<Person> findPerson(PersonId personId);

然后获得一个人的姓名的处理就会变成

  public Option<String> 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<Integer> 中可能有数字也可能没有,所以理所当然它没有办法返回一个确定的数字。所以建议大家在写非纯函数式语言的时候注意这个细节,尽量把所有的处理写在和容器交互的函数内,而不是把容器中的东西拿出来。
我有的时候突然觉得这有点像面向对象编程。对象分装了数据,你不能直接去操作数据,必须通过对象开放的接口来进行处理。

总结

说了一下自己的monad的理解。
- 可以把monad当作一种容器
- monad用来控制副作用(本人尚未理解)
- 放入容器后的东西,无法取出,只有容器才能对它操作,处理后还是以容器的形式返回
不能说有多深入或者独到的理解,只能当作自己学习笔记吧,如果今后对monad有了更好的理解,希望能再写一写。

map和flatMap

其实看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


http://www.niftyadmin.cn/n/4428414.html

相关文章

MAYA影视动漫高级模型制作全解析出_完整版PDF电子书下载 带索引书签目录高清版...

MAYA影视动漫高级模型制作全解析_页数384_出版日期2016.04_完整版PDF电子书下载 带索引书签目录高清版_13936277 下载链接 http://pan.baidu.com/s/1skA4FZf 【作 者】CGWANG动漫教育著【形态项】 384【出版项】 北京&#xff1a;人民邮电出版社 , 2016.04【ISBN号】7-115-41…

自己写deque

//deque /* what is a deque? In Chinese, its called "双端队列". Its different from a queue. Its elements can be added to or removed from either the front(head) or back(tail) ,called a head-tail linked list.输入限制deque An input-restricted deque …

ddd的战术篇: CQRS

之前的文章介绍了ddd在战术层面的要素&#xff0c;包括entity&#xff0c;value object&#xff0c;aggregate和一些设计模式如repository。在ddd中&#xff0c;repository几乎成为了等同于entity一样的基本要素。 关于aggregate与repository的回顾 aggregate是entity和value…

领域驱动设计(domain driven design)战略篇之一 战略 Bounded Context

之前的文章主要从战术层面的角度介绍了ddd。在岛国也被称为轻量级ddd。它提供了一些概念如aggregate, entity, domain event和一些设计模式如repository, specification来帮助我们建模和设计。各种战术还有能够扩展的地方&#xff0c;有机会还会再写下去。不过从这篇文章开始会…

Linux系统设置Tab键缩进为4个字符

Linux系统设置Tab键缩进为4个字符经常使用vi/vim的朋友可能会遇到&#xff0c;写脚本的时候发现按一次Tab键就缩进8个字符&#xff08;默认是8个字符&#xff09;&#xff0c;这样感觉缩进有点长了&#xff0c;这里我们可以设置下按一次Tab键&#xff0c;让它缩进4个字符&#…

领域驱动设计(domain driven design)战略篇之二 Bounded Context

之前的一篇文章谈了战略ddd的重要性与Bounded Context这个概念&#xff0c;最近在油管上看到一个2017年关于domain driven design的演讲。如下 感觉与自己现在讲的主题十分相关&#xff0c;正好在这里展开说一下。 他认为Bounded Context可能是ddd中最重要的概念。而悲剧地…

死锁及其解决方案(避免、预防、检测)

所谓死锁&#xff1a;是指两个或两个以上的进程在执行过程中&#xff0c;因争夺资源而造成的一种互相等待的现象&#xff0c;若无外力作用&#xff0c;它们都将无法推进下去。此时称系统处于死锁 死锁产生的原因&#xff1f; 1.因竞争资源发生死锁 现象&#xff1a;系统中供多…

microservice的anti-pattern

本人主要关注的是领域驱动设计(ddd)&#xff0c;一直觉得微服务算是另一个分野&#xff0c;没有特别地去关注。直到在油管上看到《领域驱动设计》的作者Eric Evans的这视频。&#xff08;视频中Evans讲解了通过微服务&#xff0c;终于实现了可靠的Bounded Context的边界。&…