js函数式编程(六)--函子
上一篇《javascript函数式编程系列(五)-- 管道与函数组合》中,我们讲到了函数式编程中的函数组合结合律、point-free以及lodash/fp模块。今天这一篇我们将javascript函数式编程最后一个知识点——函子。
今天文章的内容主要包括:
1. 什么是函子
2. 为什么要学函子?
3. MayBe函子
4. Either函子
5. IO函子
6. Monad函子(单子)
1、什么是函子
还是老规矩,我们首先得知道什么是函子。在范畴论中,函子(functor)是范畴间的一类映射,通俗地说,是范畴间的同态。在函数式编程中,函子是一个包含值和值的变形关系(这个变形关系就是函数)的特殊容器。函子通过一个普通的对象来实现,该对象具有map方法,map可以运行一个函数对值进行处理(变形关系),并最终返回一个包含了新值的新函子。仅仅靠文字描述难免太过于抽象,我们还是依旧看例子来理解。
上面的例子中定义了一个名字叫Container的函子,该函子的里面维护了一个值_value,_value是私有变量不对外公布。函子对外公布一个map方法,map方法接收一个处理_value的函数,当我们调用map方法的时候,去调用fn去处理_value,并且把处理后的结果传递给新的函子进行保存。Of方法是Container函子的静态方法,目的是用来把值放到默认最小化上下文(default minimal context)中的,同时让Container在使用的时候不用再频繁的写new,new的出现会让代码显得非常的面向对象,使用函数式编程的思想,需要避免使用new。
2、为什么要学函子
如果问为什么要学函子,我们就得知道,函子有什么用,能够解决什么问题。不知道各位小伙伴是否还记得在《javascript函数式编程系列(二)--理解纯函数》讲到的函数式编程的副作用。副作用会让一个函数变得不纯,是滋生 bug 的温床。
虽然我们尽可能地将副作用控制在可控的范围内,但是我们没有办法禁止一切副作用(既然避免不了,那么就享受它)。这里我们就了解如何使用函子去处理副作用,当然了,除了副作用,我们也可以使用函子去处理异常,异步操作等。
学以致用,在实际问题中运用学到的知识,往往能让我们印象更加深刻。我们先一起来看一个问题,先用普通的方式解决,然后转换为用函子解决。这能帮助我们更好的理解函子,先来看下面的例子。
上面的例子中,如果我们想以数据为中心,实现串行的方法去执行即:(4).add().double()。先说明为啥要这样做,很明显,一是这样的串行调用代码既简单又清晰(跟jquery的链式调用一样),二是可以实现方法优先,数据置后,构建方法时,不需要依赖参数数据。那如何才能实现一个这样的串行调用呢?要实现这样的串行调用,需要 (4) 必须是一个引用类型,因为需要挂载方法。同时,引用类型上要有可以调用的方法也必须返回一个引用类型,保证后面的串行调用。那我们可能需要这么写。
在上面的例子中我们通过new Number(4) ,创建了一个 number 类型的实例。把将要处理的值作为构造函数参数传进去赋给this._value。Number类中的add方法和double方法,都返回一个new Number的实例,以便可以使用链式调用方法。
通过上面的做法,我们已经实现了number.add().double()这样的串行调用。但是,这样的程序调用和扩展很不灵活。例如我们想让number加减一的操作,还要再写到Number类中继续添加minus函数。另外,只要我们需要对number做不同的处理,我们都得重新改造Number类,往里面添加方法。这不仅大大增加了Number的维护成本,还增加很多测试的工作量。想想就觉得很麻烦,当然了这么麻烦的事,作为一名优秀的程序员的我们当然不会去干了。
面对对象编程的思想是将数据的处理函数都写到类中,而用函数式编程成都思维,我们则需要思考如何把对数据处理这一层抽象出来,暴露到外面,让我们可以灵活传入任意函数。那么如果我们用函子的方式来解决,会是什么样的呢。
现在我们可以用
Number.of(4).map(add).map(double) 去调用,是不是觉得清爽多了。细心的小伙伴可能已经发现,如果我们能一直调用 map,那它不就是个组合函数(composition)了么。
回到函子的问题上来,我们的来做一个小总结,总的来说函子一般有以下几个特征:
1、函子(Functor)是一个容器 ,它包含一个私有值this._value。
2、函子本身具有对外接口(map方法),各种函数就是运算符,通过map方法接入容器,对容器内的值this._value进行处理。
3、函数式编程一般约定,函子有一个 of 方法,用来把值放到默认最小化上下文(default minimal context)中的。
看到这里,可能很多小伙伴就会问了,函子的也没什么嘛,不就是封装了一个简单的构造函数,暴露了一个接口而已嘛?就我个人观点而言,对于函子,我们更看中的还是一种思想,是另一种解决问题的思路。同样是函子的实现代码,一个倾向于面向对象的程序员和一个倾向于函数式编程的程序员,看到的东西肯定不一样。正如“横看成岭侧成峰”,你看问题的角度,决定了你看到的是什么。在函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器----函子(是否还记得point-free中的函数优先,数据置后)。
从上面的例子我们已经看到,函子已经帮我们做到函数组合和point-free的函数优先,数据置后了。前面我们也已经说过,函子的作用就是来解决副作用的。那我们就来继续看,哪些函子都能解决哪些副作用吧。
3、MayBe函子
想一想,如果我们上面的Container函子调用of函数的时候传入的value是null,那么就会造成函数执行异常,这是一个典型的需要去规避副作用。MayBe函子的作用就是在函子内部增加了一个处理机制,对异常的数据进行判定,从而控制副作用在允许的范围内。下面是一个MayBe函子实现的示例代码。
Maybe 函子看起来跟 Container 函子非常类似,但是Maybe 会先检查自己的值是否为空,然后才调用传进来的函数。这样我们在使用 map 的时候传给 map 的值是 null 时,代码就不会爆出错误。虽然其中某一个环节的传参有误,代码也并没有报错,实际情况的传参当然不会想演示代码这么简单,这导致了我们却没法去准确的判断是哪个环节出了错。
希望每一个小伙伴在技术成长的过程中,能将空值检查作为第二本能,相信我,你会感激它提供的安全性的。而不管怎么说,空值检查大多数时候都能防止在代码逻辑上偷工减料,让我们脱离危险,减少bug,Maybe 函子能够非常有效地帮助我们增加函数的安全性。
4、Either函子
Either的意思就是指两者中的任何一个,类似于if...else的处理。说出来可能会让你震惊,throw/catch 并不十分“纯”,异常会让函数变得不纯。当一个错误抛出的时候,我们没有收到返回值,反而是得到了一个警告,所以我们可以用Either函子去进行异常的处理。
Left 和 Right 是我们称之为 Either 的抽象类型的两个子类。在上面字符转数字的例子中,我们清楚地知道,我们得到的要么是一条错误消息,要么就是正确的。
5、IO函子
IO函子的_value是一个函数,这里是把函数作为值来处理。IO函子可以把不纯的动作存储到_value中,并延迟执行这个不纯的操作(惰性执行),然后把不纯的操作交由调用者来处理。
IO 跟之前的函子不同的地方在于,它的 _value 总是一个函数。不过我们不把它当作一个函数——实现的细节我们最好先不管。就这一点而言,我们认为 IO 包含的是被包裹的执行动作的返回值,而不是包裹函数本身。这在 of 函数里很明显:IO(function(){ return x }) 仅仅是为了延迟执行。
6、Monad函子(单子)
Modan函子也是一个很简单的概念,仅仅多了个 join 函数,为我们处理嵌套函子,先来看一个例子。
如果函数嵌套的话,我们使用函数组合去解决,如果函子嵌套的话,我们使用Monad函子解决。
最后,我们来做一个总结。写到这里,关于javascript函数式编程的知识点我们就基本上都学完了。简单回顾一下我们学过的知识点,从初识函数式的函数式编程前置知识的学习,到理解纯函数,柯理化和lodash,再到管道与函数组合,最后到函子的学习。这些都是函数式编程的基础,更多也是理论知识。“理论指导实践”,希望这些讲解能为函数式编程感兴趣的小伙伴埋下种子,后面有机会,我们再来一起学习函数式编程的实战。另外,感谢小伙伴们的一路支持,特别是能坚持订阅的小伙伴,希望各位能有所收获,有所启发。
参考链接:
《JS 函数式编程指南》
https://www.tuicool.com/wx/Ajmaqm6
https://www.jianshu.com/p/afbce25c18fb
作者介绍
庄树杰,任职于北银金融科技有限责任公司银行转型业务开发部。主要擅长前端HTML5开发和软件项目配置管理。
招聘启事
北银金融科技有限责任公司根植于北京银行,是一家致力于大数据、人工智能、云计算、区块链、物联网等新技术创新与金融科技应用的科技企业,公司充分发挥北京银行企业文化和技术积淀先天优势,通过对技术、场景、生态的完美融合,输出科技创新产品和技术服务。
现诚邀优秀人才加盟,
共享金融科技时代硕果
扫描此二维码
期待您的加入