一、 前言
我在学习一个知识点时,还是习惯性以问题解决为出发点去学习。首先先抛出一个问题,看自己是否能解决或者回答正确,检测自己知识的掌握熟悉程度和找出自己的知识盲点,针对性去学习。
在实际开发过程中,或许多多少少有使用函子去解决问题。但是因为之前没有系统性的学习过函数式编程,大脑里并没有意识到自己代码里所做的那些处理就是所谓的函子。
二、例子
讲到函子,我们首先回到我们的问题上来。之前我们执行函数(以加减乘除运算为例)通常如下:
function double(x) {
return x * 2
}
function add5(x) {
return x + 5
}
double(add5(1))
//或者
var a = add5(5)
double(a)
那现在我们想以数据为核心,一个动作一个动作去执行。
(5).add5().double()
很显然,这种以数据为出发点的链式调用,更具有可读性和维护性,运算的顺序可以任意调换。不需要从一堆嵌套的函数找出目标函数去调换到其他位置,或者增加一堆不必要的临时变量。
实现这样的调用,需要满足的条件又两个:
- (5)必须是一个引用类型,因为需要挂载方法。
- 引用类型上要有可以调用的方法
首先,我们试着去创建一个引用类型。
class Num{
constructor (value) {
this.value = value ;
}
add5(){
return this.value + 5
}
double(){
return this.value * 2
}
}
var num = new Num(5);
num.add5();
但是上面的例子存在如下问题:
- 经过调用后,返回的就是一个值了,我们没有办法进行下一步处理。
我们需要它返回一个对象。
class Num{
constructor (value) {
this.value = value ;
}
add5 () {
return new Num( this.value + 5)
}
double () {
return new Num( this.value * 2)
}
}
var num = new Num(2);
num.add5 ().double ()
我们通过new Num ,创建了一个num 一样类型的实例
- 把处理的值,作为参数传了进去从而改变了this.value的值
- 我们把这个对象返了回去,可以继续调用方法去处理函数
我们发现,new Num( this.value + 5),中对this.value的处理,完全可以通过传进去一个函数去处理。
并且在真实情况中,我们也不可能为每个实例都创建这样有不同方法的构造函数,它们需要一个统一的方法。
class Num{
constructor (value) {
this.value = value ;
}
map (fn) {
return new Num(fn(this.value))
}
}
var num = new Num(2);
num.map(add5).map(double)
我们创建了一个map的方法,把处理的函数fn传了进去。这样我们就完美的实现了我们设想的功能啦。
最后重新整理下这个函数:
class Functor{
static of(val){
return new Functor(val);
}
constructor (value) {
this.value = value ;
}
map (fn) {
return new Functor.of(fn(this.value))
}
}
Functor.of(5).map(add5).map(double)
- 我们把原来的构造函数Num的名字改成了Functor
添加了一个of 静态方法, 可以省略new关键字去创建对象
现在使用Functor.of(5).map(add5).map(double)去调用函数, 有没有觉得很简洁清爽。其实,在这个一路遇到问题,解决问题的过程中,我们已经是不知不觉的把函子的概念学习完了。上面的例子上面这个例子总的Functor就是函子。现在我们来总结一下,它有那些特点吧。
Functor是一个容器,它包含了值,就是this.value.(想一想你最开始的new Num(5))
- Functor具有map方法。该方法将容器里面的值,映射到另一个容器。(想一想你在里面是不是new Num(fn(this.value))
- 函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器——函子。(想一想你是不是没直接去操作值)
- 函子本身具有对外接口(map方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。(说的就是你传进去那个函数把this.value给处理啦)
- 函数式编程一般约定,函子有一个of方法,用来生成新的容器。(就是最后咱们整理了一下函数嘛)
三、 什么是函子(Functor)
容器:处理值与值之间的变形关系(这个变形关系就是函数)
函子: 它是一个特殊的容器,通过一个普通对象来实现,该对象具有map方法,map方法方法可以运行一个函数对值进行处理(变形关系)
容器:包含值和值的变形关系(这个变形关系就是函数)
// Functor 函子
// 函子是一个普通的对象,这个对象里面应该维护一个值,并且要对外公布一个map方法,所以我们可以通过一个类来描述函子
// 先定义一个class
// 函子是一个容器,所以这个类的名字叫Container
class Container {
// 这个类中先定义一个构造函数
// 当我们创建函子的时候,函子内部要有一个值,所以在构造函数中,要把这个值传进来
constructor (value) {
// 函子内部要把这个值存储起来
// 注意:这个值是函子内部维护,只有它自己知道,这个值永远不对外公布
// 所以在定义这个属性的时候,可以通过this._value定义这个属性(约定所有以下划线开头的这些成员,都是私有的成员,也就是这个value不想让外部访问)
// 这个值就等于初始化的时候传递进来的值
this._value = value
}
// 还要对外公布一个map方法
// map方法的作用是接收一个处理值的函数,这个函数是一个纯函数,以为需要把value传递给这个函数,由这个函数真正处理这个值
map (fn) {
// map返回的不是值,而是一个新的函子对象,在这个新的函子对象里保存新的值,始终不把值对外公布,想要处理值的话,就要给map对象传递一个处理值的函数
// 在返回这个新的函子的时候,需要把处理的值传递给Container
return new Container(fn(this._value))
}
}
// 每次创建Container的时候都需要调用new实现,这种是面向对象编程,现在是使用函数式编程,所以可以进行修改一下
class Container {
// 创建一个静态的方法of,of的作用就是返回一个函子的对象,在创建函子对象的时候,of需要接收一个参数value, of放方法中直接返回new Container,其实of方法中就封装了new关键字
static of (value) {
return new Container(value)
}
constructor (value) {
this._value = value
}
map (fn) {
// 在map方法中,可以使用of方法,因为of是一个静态方法,可以直接通过类名调用
return Container.of(fn(this._value))
}
}
let r = Container.of(5)
.map(x => x + 2)
.map(x => x * x)
console.log(r);