什么是函子

函子的英文叫做Functor,在了解函子之前我们先来说一下容器,容器包含值和值的变形关系,变性关系指的就是函数。所以容器是包含值和处理值的函数。

其实函子就是一个特殊的容器,我们可以把函子想象成一个盒子,那这个盒子里面有一个值,并且这个盒子对外要公布一个方法,这个方法我们叫做map,map方法会去接收一个参数,这个参数是一个对值进行处理的函数。这就是函子的基本概念。

首先说一下为什么要学习函子,我们知道,函数式编程是建立在数学思想上的,比如说我们的纯函数其实就是数学中的函数,那我们要学习的函子也是建立在数学的基础上。

他是建立在数学的范畴论基础上,我们这里就不讲解什么是范畴轮了,他比较复杂一些。

那我们在学习函数式编程的过程中还是没有学习如何去控制副作用,因为副作用可以让我们函数变得不纯,虽然副作用不好,但是我们又没有办法完全避免,所以我们应该尽可能的把副作用控制在可控的范围内。

我们可以通过函子来控制副作用,当然除了这个之外我们还可以通过函子去控制异常,来进行异步操作等等。

我们这里通过代码来演示一下函子,函子是一个普通的对象,这个对象里面维护一个值,并且对外公布一个map方法,所以我们可以通过一个类来描述函子,因为函子是一个容器,我们这里类的名字叫做Container。

class Container {
    map () {
    }
}

当我们创建函子的时候函子内部要有一个值,所以在构造函数中我们要把这个值传递进来叫做value,函子内部要把这个值存储起来,注意这个值是函子内部维护的,只有他自己知道,这个值是包含在一个盒子里面,不对外公布的。

我们约定所有以下划线开始的成员都是私有成员,所以我们这里用this._value接收。

class Container {
    constructor(value) {
        this._value = value;
    }

    map () {
    }
}

我们这个盒子还要对外公布一个map方法,map方法的作用是一个接收处理值的函数,那这个函数也是一个纯函数,因为我们要把这个函数去传递给这个函数,由这个函数来处理这个值。

所以我们map接收的参数叫fn,在map方法中我们要处理这个值,并且返回一个新的容器盒子,也就是一个新的函子new Container。

那在返回新的函子的时候,我们要把处理的值传递给Container,所以是fn(this._value)

class Container {
    constructor(value) {
        this._value = value;
    }

    map (fn) {
        return new Container(fn(this._value));
    }
}

那这就是一个基本的函子, 函子里面要维护一个基本的值,这个值不对外公布,另外要对外提供一个map方法,map方法要接收一个处理值的函数,并且返回一个新的函子,新函子中的值就是处理函数处理过后的值。

接下来我们创建一个Container函子,传入一个5, 接着我们想要处理函子内部的值,我们要调用map方法,调用map方法的时候需要传入一个函数,这个函数要接收一个参数,因为他要去处理Container内部的值,假设我们要让函子内部的值加1。

map执行完返回了一个新的函子,新的函子我们仍旧可以调用他的map方法,我们可以继续处理新的函子中的值,初始的时候我们给的是5,map之后得到的值是6,我们可以继续对这个值进行map处理。

const r = new Container(5).map(x => x + 1);

console.log(r);

这里的r是一个Container对象,对象里面的_value是6。我们map方法返回的最终不是值,而是一个新的函子对象,在新的函子对象里面去保存新的值,我们始终不把值对外公布,我们想要处理值的话,就给map对象传递一个处理值的函数。

那我们每次要创建一个函子的时候,我们都要调用一个new来处理,有点不太方便,我们可以把new Container这个操作封装一下。

为了和面向对象区别开来我们不使用new来创建函子,我们可以在Container中创建一个静态的方法of,这个方法的作用就是返回一个函子对象,创建函子对象的时候需要传递一个value,所以of方法接收一个value传递给对象。

其实of方法里面就封装了new关键字,这只是为了区别面向对象,所以我们不能使用new创建对象,要通过调用of创建。

这里map方法里面我们也要把new Container替换为of,因为他是静态方法,所以直接可以通过类名调用。

class Container {
    static of (value) {
        return new Container(value);
    }

    constructor(value) {
        this._value = value;
    }

    map (fn) {
        return Container.of(fn(this._value));
    }
}

let r = Container.of(5);

注意我们r拿到的是函子对象,并不是函子里面的值,我们永远也不会去取函子里面的值,如果想要对这个值处理的话,我们就会调用map方法,如果想要打印这个值,就可以在map方法传递的函数里面打印。

函子是一个具有map方法的对象,在函子里面要维护一个值,这个值永远不对外公布,就像这个值包裹在一个盒子里面,我们想要对这个值进行处理的话,我们会调用map方法。map方法执行完毕之后会返回一个新的函子。

简述函子

函数式编程的运算不直接操作值,而是由函子来完成。函子就是一个实现了map契约的对象,也就是所有函子都有一个map对象。

我们可以把函子想象成一个盒子,这个盒子里面封装了一个值,如果我们想要处理盒子中的值,那么我们就需要给盒子的map方法传递一个处理值的函数,这个函数是纯函数,他需要一个参数并且返回一个值,吧处理值的过程交给这个函数来完成。

map方法执行完成之后,他要返回一个包含新值的盒子,也就是一个新的函子,所以我们可以通过.map进行链式调用。

因为map方法始终返回的是一个函子,所有的函子都有map方法,因为我们可以把不同运算方法封装到函子中,所以我们可以引申出很多不同类型的函子,有多少运算,就有多少函子,最终可以使用不同的函子,来解决实际的问题。

上面我们写的函子存在一个问题,如果我们创建函子的时候传入了null,比如说网络请求时没有获取到数据,当我们执行map方法时,可能就会报错,这就会让我们的函数变得不纯。

因为纯函数需要有输入和输出,而当传入null的时候,函数没有输出,这个时候传入的null其实就是副作用,接下来我们要想办法去解决这个问题,也就是控制副作用。