【函数式编程】深度理解Functor函子

时间:2021-1-8 作者:admin

系列文章直通车

  1. 为什么学习函数式编程?
  2. 十分钟掌握纯函数和柯里化
  3. 五分钟了解函数组合
  4. 轻松学会PointFree
  5. 深度理解Functor函子

什么是函子

函子的英文叫做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其实就是副作用,接下来我们要想办法去解决这个问题,也就是控制副作用。

MayBe函子

MayBe是可能会是的意思。

可能会是空值的情况,我们可以通过MayBe来处理,在上一小结我们使用Functor的时候,如果出现了空值,这个时候会出现异常,而MayBe函子可以帮我们去处理空值的这种情况。

我们在编程的过程中可能会遇到很多的错误,我们需要对这些错误做处理,MayBe函子的作用就是对外部空值的情况做处理。

外部传递空值我们可以认为是一种副作用,而MayBe函子可以控制这种副作用发生。下面我们来演示一下MayBe函子。

我们首先创建一个MayBe的类,

class MayBe {
    static of (value) {
        return new MayBe(value)
    }
    constructor (value) {
        this._value = value;
    }
    map (fn) {
        return MayBe.of(fn(this._value)); 
    }
}

MayBe函子要去结局传入的值可能为null的情况,我们在map中处理这个值之前需要判断一下这个值是否为null或者undefined。

我们写一个辅助的函数用来判断当前的值是否为空,我们写一个isNothing方法判断this._value是否有值。

class MayBe {
    static of (value) {
        return new MayBe(value)
    }
    constructor (value) {
        this._value = value;
    }
    map (fn) {
        return MayBe.of(fn(this._value)); 
    }
    isNothing () {
        return ths._value === null || this._value === undefined;
    }
}

然后我们在map方法里面,在执行fn之前我们需要判断一下this._value是否为空,如果当前的值是空的话,我们不能去调用fn,我们应该返回一个值为null的函子,如果有值我们再调用fn,这里我们使用三元表达式。

class MayBe {
    static of (value) {
        return new MayBe(value)
    }
    constructor (value) {
        this._value = value;
    }
    map (fn) {
        return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value)); 
    }
    isNothing () {
        return ths._value === null || this._value === undefined;
    }
}

此时如果我们传入的是null,我们代码不会报错,而是会返回一个值为null的新的MayBe函子。

接下来我们再来看MayBe函子的一个问题,虽然我们可以处理空值的问题,但是如果多次调用map方法的时候,哪一次出现了空值,我们是不太明确的。

Either函子

Either单词的意思是两者中的任何一个,我们在使用Eight处理问题的时候,就相当于if else 的处理过程。

我们之前在使用MayBe函子的时候,当我们在传入null的时候,我们不会去处理外部的函数fn,仅仅返回一个值为null的函子,但是不会给出任何有效的信息,他不会告诉我们是哪里出了问题,出了什么问题。

我们可以使用Either这个函子来解决这个问题,当出现问题的时候Either会给出我们有效的提示信息。

我们一个函数中如果出现异常,会让这个函数变得不纯,那我们Either函子也可以用来处理异常,下面我们来看一下,Either函子如何实现。

我们在使用Either函子的时候,因为他是二选一,所以我们需要定义两种类型,一个是Left一个是Right,在这两个类中我们分别要去定义静态的of方法去放回当前这个对象,还有构造函数和map方法。

在Left的map方法中,这里比较特殊,直接返回了this,Right的map方法和之前保持一致。

class Left {
    static of (value) {
        return new Left(value);
    }
    constructor (value) {
        this._value = value;
    }
    map (fn) {
        return this;
    }
}

class Right {
    static of (value) {
        return new Right(value);
    }
    constructor (value) {
        this._value = value;
    }
    map (fn) {
        return Right.of(fn(this._value));
    }
}

我们观察这两个函子可以发现,他们和我们之前的函子基本上是一样的,都有of方法,都有constructor和map,其实我们在写的时候都可以继承之前的Container的,我们这里就不去继承了,方便演示。我们分别创建一个函子,打印看一下。

let r1 = Right.of(12).map( x => x + 2);
let r12 = Left.of(12).map( x => x + 2);

console.log(r1); // ...14
console.log(r2); // ...12

这里我们听过打印Left和Right创建的函子可以发现Left返回的是我们直接传入的值,没有做任何的处理。Left当中的map方法是直接返回的当前对象this,并没有调用当前传入的fn。

为什么要这么做呢,我们可以在Left中嵌入一个错误消息,下面我们演示一个可能会发生错误的函数,比如我们要去把一个JSON形式的字符串,转换成一个JSON对象。

因为调用JSON.parse的时候可能出现异常,所以我们使用try…catch。如果发生异常我们不去处理的话他不是一个纯函数,现在我们希望用函数式的方式来处理,所以我们需要些一个纯函数。

这里我们在try里需要return一个函子,我们会把我们转换后的 结果交给这个函子,将来在这个函子内部去处理,我们直接返回一个正确的值,Right.of。

我们通过Right.of创建的函子,当我们调用map方法的时候,map方法传入的这个函数会去处理我们传的这个值。这里我们传递把字符串转换成对象的值JSON.parse(str)。

如果出现错误我们也要在catch中返回一个值,因为纯函数需要有输出,这个时候我们也是要返回一个函子,Either中的Left用于处理异常。

function parseJSON (str) {
    try {
        return Right.of(JSON.parse(str));
    } cache (e) {
        return Left.of({
            error: e.message
        })
    }
}

这样我们的parseJSON就写完了,这就是Either对异常的处理。

IO函子

至此,我们已经对函子有一个简单的认识,我们可以把函子想象成一个盒子,盒子里保存一个值,通过调用盒子的map方法可以传入一个函数,通过这个函数对盒子里面的值进行处理。

接下来我们来学习一下IO函子,也就是输入输出的函子,他和之前函子不同的地方在于,他内部的value始终是一个函数。

IO函子就是把不纯的操作都存储在value中,value中存储的是函数,在函子内部并没有调用这个函数,通过IO函子是延迟执行了这些不纯的操作,也就相当于惰性执行。

通过IO函子先包装一些函数,当我们需要的时候,再来执行这些函数,因为IO函子中存储的函数有可能是不纯的,但是通过IO函子包装起来的话,我们当前的操作就是一个纯的操作。把不纯的操作延迟到调用的时候。

有了IO函子就可以把各种不纯的操作装进笼子里,但是这些不纯的操作,最终都要执行的,我们可以把这些不纯的操作交给调用者来处理。

使用IO函子的时候,先创建一个IO的类,构造函数接收一个函数,这和之间是不一样的,我们这里把这个函数存起来。在of方法中也和之前不一样,of方法接收的是一个数据,在of方法里面返回一个IO函子,在构造函数中传入一个函数,这个函数中返回数据。

通过of方法我们可以看出,IO函子最终还是想要返回的数据,只不过这里通过一个函数把这个值包裹起来了,IO函子的value保存的是这个函数。这个函数返回的是一个值,他把求值的过程做了延迟处理,当我们想要这个值的时候再调用IO函子的value函数。

这里的map方法和之前也是不同的,map韩式接收一个fn函数,在map方法里面通过调用IO的构造函数来创建一个IO的函子,参数里面调用了fp的flowRight将fn和value组合起来,最终得到新的函数传递给IO的构造函数,得到一个IO函子,并且返回。

const  fp = require('lodash/fp');

class IO {
    static of (x) {
        return new IO(function() {
            return x;
        })
    }
    consturctor (fn) {
        this._value = fn;
    }
    map (fn) {
        return new IO(fp.flowRight(fn, this._value));
    }
}

我们首先调用IO的of方法返回一个函子,of方法接收一个值,我们可以传入一个process(Node环境才可以的)。

接下来我们调用map方法,来获取process中的exePath,就是当前node环境中进程的执行路径。

let r = IO.of(process).map(p => p.execPath);

console.log(r);

可以发现我们这里返回的是一个IO函子,这个IO函子中的value保存的是函数function,我们来分析一下这个函数是谁。

当我们调用IO.of的时候我们传入了process对象,在of我们返回一个函子,并且把process包装到函数中,接着调用map方法,在map方法中调用flowRight把of中包裹process的函数组合上map传入的函数。返回一个IO函子。

这个function就是当前函子的value也就是组合之后的函数。

那接下来我们想要获取这个执行结果,想要调用IO函子中的函数,我们看到IO中的value就是一个函数,所以我们可以r._value()直接调用。

let r = IO.of(process).map(p => p.execPath);

console.log(r._value());

这里总结一下,IO函子内部包装了一些函数,我们在传递函数的时候有可能这个函数是一个不纯的操作,我们不关心这个函数是否纯净,IO函子在执行的过程中返回的结果始终是一个纯的操作。

IO中有可能包裹了一些不纯的操作,但是当前的执行始终是一个纯的操作,调用map方法的时候始终会返回一个IO函子,但是IO函子的value属性里面保存的一些函数,因为他里面最终要去合并很多函数,所以他可能是不纯的。我们将不纯的操作延迟到了调用的时候,也就是通过IO函子控制了副作用在可控的范围内发生。

Monad函子

Monad单词的意思是单细胞动物的意思,我们经常把他翻译成单子。

在学习Monad之前我们先来说下IO函子的一个问题,在linux系统中有个cat命令,是读取文件内容并且把他打印出来,我们写一个函数来模拟这个命令。

class IO {
    static of (x) {
        return new IO(function() {
            return x;
        })
    }
    consturctor (fn) {
        this._value = fn;
    }
    map (fn) {
        return new IO(fp.flowRight(fn, this._value));
    }
}

我们先来写一个读取文件的函数,再来写一个打印的函数,然后把他们组合成一个函数。

因为读取文件存在副作用,会让函数变得不纯,所以我们这里使用IO函子, 也就是我们把读取文件的过程延迟执行。

在打印函数中我们也返回IO函子,延迟执行

let readFile = function (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8');
    })
}

let print = function (x) {
    return new IO(function () {
        console.log(x);
        return x;
    })
}

下面我们将这两个函数合并成cat。

let cat = fp.flowRight(print, readFile);

let r = cat('package.json');

console.log(r);

这里的调用之后,我们readFile返回一个IO函子,IO函子传入print函数之后,这个函数返回了一个函子,函子中的value就是readFile的函子,所以这里拿到的是嵌套函子。

下面我们去执行函子里面的函数,之前我们介绍过可以通过调用_value()执行。

console.log(r._value());

当我们执行_value的时候得到的是readFile函数返回的IO函子, 因为readFile返回值会传递给print函数。

我们现在想要拿到文件的结果,我们还需要再调用一次_value方法,这个方法才是readFile中的_value

console.log(r._value()._value());

至此我们就获取到了文件内容,但是问题是我们在调用嵌套函子的时候非常的不方便,我们需要._value()._value(),这看起来很怪异。

下面我们来介绍一下Monad来解决一下上面的问题。

Monad是可以变扁的Pointed函子,那什么是变扁呢,上面我们出现了一个问题,就是函子嵌套的话,我们调用起来会很不方便,变扁就是解决函子嵌套的问题。

之前学过,如果函数嵌套的话,可以使用函数组合来解决这个问题,如果函子嵌套就可以使用Monad。

如果一个函子同时具有join和of两个方法,并且遵守一些定律的话,就是一个Monad。

of我们很熟悉,join也不复杂,他直接就返回了我们对_value的调用。

我们将IO类改造成Monad,添加一个join方法,join方法不需要任何参数,这里只是返回_value的调用。

class IO {
    static of (x) {
        return new IO(function() {
            return x;
        })
    }
    consturctor (fn) {
        this._value = fn;
    }
    map (fn) {
        return new IO(fp.flowRight(fn, this._value));
    }
    join () {
        return this._value();
    }
}

接着我们再写一个flatMap方法,我们在使用Monad的时候经常会把map和join联合起来去使用,因为map的作用是把当前的函数和函子内部的value组合起来,返回一个新的函子,map在组合这个函数的时候,这个函数最终也会返回一个函子,所以我们需要调用join把他变扁,把他拍平。

flatMap的作用就是同时调用map和join,flatMap要调用map方法,map方法需要一个fn参数,所以flatMap也需要一个参数,在flapMap执行完成之后,我们要去调用join,并且把join执行的结果,也就是这个函子返回。

当我们调用map的时候,我们就把value和fn进行合并,合并之后返回一个新的函子,在这个函子包裹的函数最终也会返回一个函子,所以我们再去调用join()。

class IO {
    static of (x) {
        return new IO(function() {
            return x;
        })
    }
    consturctor (fn) {
        this._value = fn;
    }
    map (fn) {
        return new IO(fp.flowRight(fn, this._value));
    }
    join () {
        return this._value();
    }
    flatMap(fn) {
        return this.map(fn).join();
    }
}

至此Monad就写完了,下面我们看下如何去使用。

let r = readFile('package.json');

当我们调用readFile的时候他会生成一个函子,这个函子包裹了我们读文件的操作,然后我们将读文件的操作和打印的操作合并起来。

我们要调用map还是flatMap取决于我们要合并的函数返回的是值还是函子,如果是指就调用map,函子就调用flatMap。

let r = readFile('package.json').flatMap(print);

我们调用完readFile会返回一个IO函子,它里面封装了一个读取文件的函数,接下来调用flatMap我们传入print,我们看下flatMap执行,当我们调用flatMap的时候传入了print,在flatMap里面调用了this.map, 我们将print和当前函子内部的value进行合并,合并之后返回了一个新的函子。

当我们调用完map之后,我们得到一个函子,并且这个函子中报国的函数最终返回的还是一个函子,接着我们调用join,他就是调用返回这个函子的value。

所以flatMap返回的就是print的函子,最后我们想要获取print的文件内容,我们再调用一下join就可以了, 因为join就是在调用内部的value。

let r = readFile('package.json').flatMap(print).join();

这里可能看起来比较麻烦,不过在实际运用中是不需要关心函子内部实现的,只需要调用函子的api实现想要的功能就可以了。

假设我们读取完文件内容,我们想把文件的字符串全部转换成大写,我们直接在readFile后面调用map方法就可以了,因为map方法作用是处理函子内部value的值。

Task函子

Task函子可以帮我们控制副作用进行异常处理,还可以处理异步任务,因为异步任务会带来回调地狱问题。

使用Task函子可以避免出现回调的嵌套,因为异步任务的实现过于复杂,所以这里我们使用folktale库中提供的Task函子来进行演示。

folktale是一个标准的函数式编程库, 他和lodash,ramda不同的是,它里面没有提供很多功能性的函数,他只提供了和函数式处理相关的操作,例如compose,curry等,他还提供了一些函子,例如Task,Eight,MayBe等。

我们先来演示一下folktale中的compose和curry如何使用。

const { curry } = require('folktale/core/lambda');

let f = curry(2, (x, y) => {
    return x + y;
});

console.log(f(1, 2));

这里面的curry和lodash有所不同,这里面接收两个参数,第一个参数用来指明函数参数有几个参数。文档上说这里传递第一个参数的目的是为了避免一些错误。

下面演示下compose,这是函数组合的意思,我们这里不自己写函数了,直接使用lodash的函数,我们把数组中的第一个元素取出来,并且转换成大写。

const { compose } = require('folktale/core/lambda');
const { toUpper, first } = require('lodash/fp');

let f = compose(toUpper, first)

console.log(f(['a', 'b']));

这里的compose函数和lodash中的flowRight用法是一样的。

接下来我们介绍下folktale中提供的Task函子处理异步任务。folktale2.x中的Task和folktale1.x中的Task使用方式区别很大,1.x中的用法更接近现在使用的函子,我们这里以2.x来演示。无非就是api的不同,我们可以通过查阅文档来了解使用。

我们通过读取一个文件,来演示下异步任务。具体在folktale的什么位置我们需要自己翻阅文档去了解。

这里提供的task是一个函数形式,这个函数会返回一个函子对象,在1.x中提供的是一个类。

接着我们写一个读取文件的函数readFile, 这个函数接收一个文件路径参数,返回一个task函子。

task这个函数本身需要接收一个函数,而这个函数的参数是固定的,叫做resolver,resolver是一个对象,它里面有两个方法,一个是resolve,执行成功之后调用的方法,还一个reject,执行失败之后执行的方法,他使用起来非常像Promise。

我们在这个函数中读取文件。

const { task } = require('folktale/concurrency/task');
const fs = require('fs');

function readFile(filename) {
    return task(resolver => {
        fs.readFile(filename, 'utf-8', (err, data) => {
            if (err) {
                resolver.reject(err);
            } else {
                resolver.resolve(data)
            }
        })
    })
}

当我们调用这个readFile函数的时候,他会返回一个Task函子,当我们想要读取文件的话,我们需要调用Task函子提供的run方法。

readFile('package.json').run();

我们可以通过listen方法监听文件读取状态,这里传入一个对象,对象中包括onRejected回调和onResolved回调。

readFile('package.json').run().listen({
    onRejected: err => {
        console.log(err);
    },
    onResolved: value => {
        console.log(value);
    }
})

此时我们再去执行代码,就会发现这个文件已经读取到了,我们如果想要处理拿到的值,我们可以在run之前调用一下Task函子的map方法,在map方法里面可以处理拿到的结果。这样更符合函数式编程。

在map方法里我们会去处理我们拿到这个文件的返回结果,所以我们在使用函子的时候,我们就没有必要去想它里面的实现机制了,之前是自己写函子,我们了解内部实现机制,而我们实际开发的过程中我们就直接使用。

readFile('package.json').map(value => {
    console.log(value); // 处理文件
    retrun value;
}).run().listen({
    onRejected: err => {
        console.log(err);
    },
    onResolved: value => {
        console.log(value);
    }
})

Pointed函子

Pointed函子指的是实现了of静态方法的函子,那我们之前所写的函子都是实现了of方法的,所以他们都是Pointed函子。

之前说of方法是为了避免使用new啦创建对象,避免我们的代码看起来很面向对象,但是of方法更深层的含义是,他是用来把值放到一个上下文中,然后在上下文中处理我们的值。(把值放到容器中,使用map来处理值)。

假设我们的值是2,我们通过of方法可以把这个值放到一个盒子里,那这个盒子我们就叫做上下文,其实就是我们的函子。

假设我们有一个Container函子,这个函子有一个of方法,他就是一个Pointed函子,of方法的作用是帮我们把值包裹到一个新的函子里面,并且返回。那我们称这个返回的结果就是上下文。

当我们调用of方法时候我们获得一个上下文,将来我们在这个上下文里面去处理这个数据。

这就是Pointed函子,他比较简单,就是一个概念而已,我们早已经在使用了。

函数式编程总结

到这里函数式编程我们就讲完了,下面我们来总结一下,整个函数式编程我们分为四个部分。

  • 认识函数式编程

也就是函数式编程的概念。

函数式编程是一种编程范式,或者说编程思想,他和面向对象编程是同一级别的,我们想要掌握一门编程思想是需要花费很长时间的,我们可以把我们掌握的直接在工作中运用,不需要把所有东西都用函数式来写,因为这看起来太困难了。

函数式编程的核心思想是,把运算过程进行抽象成函数,在编程的过程中是面向函数进行编程的。

现在我们要学习函数式编程是因为像vue或者react,他们内部都已经使用了部分的函数式编程的思想,所以学习函数式编程有助于我们去使用vue或者react。

  • 函数相关复习

函数是一等公民指的是,函数也是对象,所以我们可以把函数像值一样去处理,函数也可以作为另一个函数的参数,或者返回值。

高阶函数其实就是把函数作为参数或者把函数作为返回值,我们在使用柯里化或者函数组合的时候其实就是基于高阶函数的,至于闭包他是无处不在的。

  • 函数式编程基础

lodash是一个函数式编程的库,它里面提供了很多函数式编程的方法,可以辅助我们开发。

纯函数指的是给一个函数输入相同的参数,总能得到相同的输出,并且没有任何的副作用,纯函数其实就是数学中的函数,可以把一个值映射成另一个值,纯函数可缓存,可测试并且方便并行的处理。

柯里化可以对函数进行降维处理,也就是我们可以把多元函数转化成一个一元函数,我们把多元函数转换成一元函数的目的是我们在函数组合的时候要去使用。

我们理解了管道之后对于我们学习函数组合是有帮助的,我们可以把一个函数想象成一个处理数据的管道,我们给这个管道输入一个数据,当这个数据经过这个管道之后会得到一个相应的结果,函数组合其实就是这样来处理的。函数组合可以把多个一元的函数组合成一个新的函数,组合成一个功能更强大的函数。

  • 函子

函子可以帮助我们控制副作用,进行异常处理或者异步操作等等,函子的概念非常简单,我们可以把函子想象成一个盒子。这个盒子里面包裹着一个值,我们想要对这个值进行处理的话,我们需要调用这个盒子给我们提供的map方法。

map方法接收一个函数类型的参数,我们传递的这个函数,就是去处理值的这个函数。

我们通过Functor演示了函子的基本使用,后面我们又学习了MayBe函子,MayBe函子的作用是帮我们处理空值的异常,我们想要对异常进行处理的话,我们创建了Either函子,这些函子内部的value都是保存一个值。

后面我们创建了IO函子,他的value里面存储的是一个函数,使用IO函子可以延迟执行一个函数,使用IO函子可以控制副作用。

再后面我们学习了Task,在学习Task的时候,介绍了一个函数式编程的库folktale,这个库没有提供功能性的方法,他提供的方法都是方便函数式处理的,这个库还提供了一些函子,比如说Task,Task的作用是进行异步处理,帮助处理异步任务。

Monad函子的作用是解决函子嵌套的问题,如果一个函子具有静态的of方法,并且还有一个join方法,那他就是一个Monad。


欢迎关注,更多系列文章

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。