javascript函数式编程-------函子(Functor)

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

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

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

Functor(函子)

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

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

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

map方法接收一个处理值的函数(纯函数),调用map方法就会调用这个处理值的函数从而得到新的值,并返回一个储存了新值的新函子(new Container)。

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

let obj = new Container(5).map((x)=>{
    return x+2;
}).map(x=> x*x)
console.log(obj)

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

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

总结:

        函数式编程不直接操作值,而是由函子完成;

        函子是一个有map契约的对象;

        可以把函子想象成一个盒子,这个盒子里储存了一个值;

        map方法接收一个处理值的函数,由处理值的函数去得到一个新的值;

       最终map方法返回一个储存了新值的新函子(盒子)。

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

 

 

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

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

Maybe 函子

在编程的过程中可能会遇到很多错误,我们需要对这些错误进行处理;

maybe函子的作用就是对空值进行处理,外部传入控制可以看作是一种副作用,maybe函子可以控制这种副作用的发生;

来看maybe函子代码:( MayBe函子要去结局传入的值可能为null的情况 )

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 this._value === null || this._value === undefined;
    }
}
let obj = new Maybe(5)
        .map(x=> null)
        .map(x=> x*x)

console.log(obj)

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

但是,又引申出一个新的问题,如果多次调用map方法,在什么位置出现的null,我们是不知道的,接下来看Either函子。

Either函子

Either 两者中的任意一个,使用Either 函子处理问题时,类似于if...else...的处理方式。

上面的maybe函子接收到空值时,不会处理map的参数fn,仅仅返回一个值为null的函子,不会给出有效信息,我们哪块出了问题,出了什么问题。我们可以用either函子解决这个问题,当出现问题时Either函子会给出有效的提示信息。

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

        我们在使用Either函子的时候,因为它是二选一,所以我们需要两个类: 可以看到在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) );
    }
}

为什么left的map方法中不执行传入的fn方法,而要直接返回this呢,因为我们可以在Left中嵌入一个错误消息。

下面演示一个可能会发生 错误的情况,比如我们把一个json字符串转为json对象。

function parseJson(str){
    try{
        return Right.of( JSON.parse(str) )
    } catch (e){
        return Left.of( { error:e.message } )
    }
}
const obj = parseJson("{name:wjp}");
console.log(obj)   //Left { _value: { message: 'Unexpected token n in JSON at position 1' } }

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

如果没有发生错误就返回一个Right的新函子,这个新函子储存了处理后的值。

如果发生错误也返回一个Left的新函子,这个新函子储存了相应的错误信息。

所以通过Either函子可以去处理异常,并且可以在一个函子中记录下来异常信息。

 

I/O函子

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

接下来我们来学习一下IO函子,IO是input,output的意思,也就是输入输出的意思,和之前函子不同的地方在于:

1.它内部的value值始终是一个函数。(函数是一等公民);

 2.IO函子可以把不纯的操作存储到 _value 中,在函子中并没有调用它,所以通过IO函子来延迟这个不纯的操作,保证当前的操作都是纯函数,

3.IO函子可以把各种不纯的操作装进笼子里,但是这些不纯的操作,最终都要执行的,我们可以把这些不纯的操作交给调用者来处理。也就是通过IO函子先包装一些函数,当我们需要的时候,再来执行这些函数

下面是一个IO函子简单的例子:

of方法:和之前of方法不一样,这里的of方法接收了值,返回一个IO函子,但是在创建IO函子的时候传入的是一个函数,这个函数被调用时才会返回数据(也就是这个函数把传过来的值给包裹了起来);

constructor构造函数:构造函数被调用的时候传入的是一个函数(of方法被调用创建实例的时候发生),其实看到of方法能知道IO函子最终还是想把数据给我们返回(_value属性),只不过 _value 是一个函数,_value这个函数被调用时才会得到数据;

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

const fp = require("lodash/fp")
class IO{
    static of(value){
        return new IO(function(){
            return value;
        })
    }
    constructor(fn){
        this._value = fn;
    }
    map(fn){
        return new IO( fp.flowRight( fn,this._value ) )
    }
}

下面用IO类的of方法创建一个IO函子并返回,参数传入一个普通对象,然后打印一下这个函子可以发现它的 _value 属性(也就是储存在函子内部的值)是一个函数,返回值是最初传入的数据,调用它时返回数据。

const obj = IO.of( {name:"wjp"} )
console.log( obj._value ) // IO { _value:[Function] }
console.log( obj._value() ) //{name:"wjp"}

接下来调用这个函子中的map方法,传入一个处理数据的函数fn,fn接收一个参数就是一开始调用of函数创建函子时的数据。

map方法里面:通过lodash库fp模块 的flowRight方法----把传入的fn方法和当前函子的_value函数组合成为一个新的函数;

                         把fn和_value进行组合后的函数 当作参数传入构造函数中,返回新的函子;(新函子的_value就是组合后的函数)

const obj2 = IO.of( {name:"wjp"} )    //创建函子1
                .map( x=>x.name )     //通过函子1的map方法创建函子2
console.log(obj2._value()) // wjp

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

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

Task函子(异步执行)

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

因为异步任务的实现过于复杂,我们先使用 folktale 中的 Task 来演示。

folktale 一个标准的函数式编程库 ;和 lodash、ramda 不同的是,他没有提供很多功能函数 只提供了一些函数式处理的操作,例如:compose、curry (函数组合和柯里化)等,一些函子 Task、Either、 MayBe 等

我们先来演示一下folktale中的compose和curry如何使用。这里面的curry和lodash有所不同,这里面接收两个参数,第一个参数用来指明函数参数有几个参数。文档上说这里传递第一个参数的目的是为了避免一些错误。

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

let fn1 = curry(2,(a,b)=>{
     return a+b;
});
console.log(fn1(1,2))  //3
console.log(fn1(1)(2))  //3

let fn2 = compose( toUpper,first );
console.log(fn2(["web","curry"]));  //WEB

接下来使用folktale中的提供的task函子来执行异步任务:

这里提供的task是一个函数形式,这个函数会返回一个函子对象。

接着我们写一个读取文件的函数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函子,他比较简单,就是一个概念而已,文章的一开始早已经在使用了。

Monad函子

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

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

因为读取文件会存在副作用,会让函数变得不纯,所以这里采用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函子,延迟执行 (这里的fs.readFileSync采用同步读取文件的方式)

function readFile(fileName){
  let end = fs.readFileSync(fileName,"utf-8");
  return IO.of( end )
}
function print(value){
  console.log(value);
  return IO.of( value )
}

然后将两个函数组合,得到组合后的新函数;

传入文件名并执行cat,调用执行后:readFile会返回一个函子,这个函子会传入print函数中,print会再返回一个函子,不过print返回的函子包裹了readFile的函子,也就是print返回的函子的_value就是readFile的函子,所以这里拿到的是嵌套函子。

let cat = fp.flowRight(print,readFile);
let r = cat('package.json');

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

console.log(r._value());

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

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

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

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

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

首先:我们来看一下上面cat函子,它的结构是这样的(将就看吧,我第一次看感觉有点绕)

readFile (IO函子): {
	_value: function(){
		return print (IO函子): {
			_value: function(){
				return "文件数据"
			}
		}
	}
}

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的值。

Monad函子总结:

具有静态 的of方法并且具有join方法的函子;

当一个函数返回一个函子的时候我们就要想到monad;

它可以帮我们解决函子嵌套的问题,当我们想要合并一个函数并且这个函数返回一个值,我们调用map方法;当我们想要合并一个函数但是这个函数返回一个函子,我们调用flatMap方法;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值