五、函子

函子概念

  • 是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运 行一个函数对值进行处理(变形关系)
  • 所谓的容器,是指包含值的变关系(这个变形关系就是函数)【容易让人想到Vuex或者Redux中的state和mutation或者reducer,又或者是react Hooks中的useState返回的两个值,一个是值,一个是修改值的方法】
  • 作用:控制函数式编程的副作用,异常处理,异步操作等。

函子的基本结构:

  • 面向对象的方式来创建一个函子
class Container{
    constructor(value){
        this._value = value
    }
    map(fn){
        return new Container(fn(this._value))
    }
}

let r = new Container(5)
  .map(x => x+1)
  .map(x => x*x)

console.log(r)     // Container {_value: 36}

// map的作用就是接收一个处理value值的函数作为参数,并且返回一个新的函子(类),这个函子的构造器中也会有value这个属性,并且属性值是上一个函子中的value经过map方法后,被map方法中的处理函数fn处理过后得到的值。map方法返回的是一个新的函子,所以可以一直链式调用map下去,可以填入不同的map中的fn函数,得到不同的处理value的值。
  • 函数式编程的方式
// 一个容器,包裹一个值
class Container {
    // of 静态方法,可以省略 new 关键字创建对象
    static of (value) {
        return new Container(value)
    }
    constructor(value) {
        this._value = value
    }
    // map 方法,传入变形关系,将容器里的每一个值映射到另一个容器
    map(fn) {
        return Container.of(fn(this._value))
    }
}
// 测试
Container.of(3)
    .map(x => x + 2)
    .map(x => x * x)

// of用来生成定义一个函子(类似于初始化操作),map用来操作函子中的数据生成新的函子。这个函子其实就是对象的数据格式。

特征:

  • 函数式编程的运算不直接操作值,而是由函子完成

  • 函子就是一个实现了 map 契约的对象

  • 我们可以把函子想象成一个盒子,这个盒子里封装了一个值 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这 个函数来对值进行处理

  • 最终 map 方法返回一个包含新值的盒子(函子)

  • 如果在普通函子中传入了null或者undefined会导致出错:

// 值如果不小心传入了空值(副作用)
Container.of(null)
.map(x => x.toUpperCase())
// TypeError: Cannot read property 'toUpperCase' of null

引出MayBe 函子来解决这个问题

MayBe函子

  • 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理

  • MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)

  • 在普通函子的map方法中加入了判断null和undefined的函数isNothing,如下:

class MayBe {
    static of (value) {
        return new MayBe(value)
    }
    constructor(value) {
        this._value = value
    }
    // 如果对空值变形的话直接返回 值为 null 的函子
    map(fn) {
        return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }
    isNothing() {
        return this._value === null || this._value === undefined
    }
}


// 传入具体值
MayBe.of('Hello World')
    .map(x => x.toUpperCase())
// 传入 null 的情况
MayBe.of(null)
    .map(x => x.toUpperCase())
// => MayBe { _value: null }
  • 但是如果在多次map过程中出现了null会导致使用者不知道在哪个环节出现了null,故引出了Either函子。
MayBe.of('hello world')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
// => MayBe { _value: null }

Either函子

  • Either 两者中的任何一个,类似于 if…else…的处理
  • 异常会让函数变的不纯,Either 函子可以用来做异常处理

Either函子示例:

// 建立两个类,一个left一个right,这两个共同构成了Either函子,try...catch结构中进行二选一操作。

class Left {			// Left函子用来存储错误
    static of (value) {
        return new Left(value)
    }
    constructor(value) {
        this._value = value
    }
    map(fn) {
        return this	// left是用来记录错误的函子,他的of方法时用来返回一个新的函子,这个函子中的value保存的是错误信息
    }  // 这里的left的map方法不会对函子进行处理,而是当发生错误时,走catch这条路,直接返回生成的新的、带有错误信息的函子。
}	

class Right {		// Right函子用来数据处理
    static of (value) {
        return new Right(value)
    }
    constructor(value) {
        this._value = value
    }
    map(fn) {
        return Right.of(fn(this._value))
    }
}

// 测试
function parseJSON(json) {
    try {  // 而当正确时走的是try这条路,输出正确的结果并通过right函子的map方法进行处理。
        return Right.of(JSON.parse(json));
    } catch (e) {
        return Left.of({
            error: e.message
        });
    }
}
let r = parseJSON('{ "name": "zs" }')
    .map(x => x.name.toUpperCase())
console.log(r)



IO函子

  • IO 函子中的 _value 是一个函数,这里是把函数作为值来处理

  • IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作

  • 把不纯的操作只有在调用的时候才会暴露出来,交给调用者来处理

示例:

const fp = require('lodash/fp')

class IO {
    static of (x) {
        return new IO(function () {   // IO函子内部保存的值是个函数
            return x
        })
    }
    constructor(fn) {           // 将IO函子接收的函数保存到_value中
        this._value = fn
    }

    map(fn) {          // map方法是要对函子中已经保存的_value值进行处理,来生成新的函子
        return new IO(fp.flowRight(fn, this._value))
    }
}

// 调用
let io = IO.of(process).map(p => p.execPath)
console.log(io._value())


// 由于IO函子在接收函数值时就已经通过of方法进行了一次包装,生成了新的IO函子
// 我们后续的操作都是在这个新生成的IO函子中进行的操作
// 所以接收的函数process参数已经被of方法生成的IO函子时的匿名函数包装处理了一次,这个匿名函数等同于constructor中的fn,也就是_value中的fn
// 然后就是再把被_value处理过的process再用map传递的处理函数再处理一次,故选用函数组合。
// 所以和map传入的fn组合成一个新的函数,最后生成一个新的、经过了包装处理、map处理的新IO函子
  • IO函子的作用
    • 把不纯的函数包装起来,保证每次输入都返回一个不依赖于外部资源IO函子(本质是个对象)
    • 最后延迟执行不纯的函数,同时保留函数对值的处理,这个函数处理就是map传递的函数参数

Folktale

  • folktale 一个标准的函数式编程库 和 lodash、ramda 不同的是,他没有提供很多功能函数

  • 只提供了一些函数式处理的操作,例如:compose、curry 等(用法上和lodash有差异),一些函子 Task、Either、 MayBe 等

  • 可以使用Folktale库提供的Task函子进行异步操作

示例:

const { compose, curry } = require('folktale/core/lambda')   //  注意这个路径
const { toUpper, first } = require('lodash/fp')

// 第一个参数是传入函数的参数个数,lodash用的_.curry()方法
let f = curry(2, function (x, y) {
    console.log(x + y)
})
f(3, 4)
f(3)(4)

// 函数组合,用的是compose方法,lodash中用的是_.flowRight()方法
let f = compose(toUpper, first)
f(['one', 'two'])

Task函子

  • 使用Task函子来执行异步任务
  • Task 异步执行 folktale(2.3.2) 2.x 中的 Task 和 1.0 中的 Task 区别很大,1.0 中的用法更接近我们现在演示的 函子 这里以 2.3.2 来演示
const fs = require('fs')
const { task } = require('folktale/concurrency/task')  // 返回的是一个函子对象,1.0中返回的是个类
const {split, find} = require('lodash/fp')

function readFile(filename) {
    return task(resolver => {  // 返回一个task函子,参数是一个resolver对象,它有着类似于Promise的结构(resolve和reject)
        fs.readFile(filename, 'utf-8', (err, data) => {
            if (err) resolver.reject(err)  // 如果出错就把错误传递出去,node里面是错误优先所以先判断这个
            resolver.resolve(data)  // 如果成功就把读取的数据传递出去
        })
    })
}

// 调用 run 执行
readFile('package.json')  // 返回一个task函子,使用run方法才能执行读取操作
    .map(split('\n'))   // 对task函子使用函子中的map方法进行处理,处理方法时从外部传进去的【先用map处理,再用run来执行】
    .map(find(x => x.includes('version')))  // 对task函子使用函子中的map方法进行进一步的处理处理
    .run()  // 执行task函子
    .listen({  // 监听执行状态
        onRejected: err => {   // 失败时的执行
            console.log(err)
        },
        onResolved: value => {  // 成功的执行
            console.log(value)
        }
    })

Pointed 函子

  • 实现了of方法的函子就是pointed函子。就是实现了of静态方法的函子

  • 更深层的含义是 of 方法用来把值放到上下文 Context(把值放到容器中,使用 map 来处理值)

    如图:

在这里插入图片描述

  • Pointed 函子是实现了 of 静态方法的函子 of 方法是为了避免使用 new 来创建对象。如下:
class Container {
    static of (value) {
        return new Container(value)
    }
}

......

Container.of(2) // 这里的Container就是一个上下文,map方法其实是对这个上下文中的数据进行操作的
    .map(x => x + 5)

IO函子存在的问题

cat:读取文件内容并打印

模拟实现cat函数:

onst fs = require('fs')
const fp = require('lodash/fp')
let readFile = function (filename) { // readFile中的readFileSynce函数是个不纯的函数,依赖于外部资源
    return new IO(function () {		// 将其包裹起来存到filename生成的IO函子的_value中
        return fs.readFileSync(filename, 'utf-8')
    })
}
let print = function (x) {  // print中的x就是上一步readFile中返回的IO函子,将它存到了print生成的新IO函子的value中
    return new IO(function () {
        console.log(x)
        return x
    })
}

// IO(IO(x))
let cat = fp.flowRight(print, readFile)  // 拿到了组合的函数,这个函数的格式就是经过两个IO函子处理后的新的IO函子IO(IO(x))
// 调用
let r = cat('package.json')._value()._value()
console.log(r)

用这种IO函子处理嵌套问题时会导致处理麻烦,频繁调用。

Monad函子

  • Monad函子是可以变扁的pointed函子,IO(IO(x))

  • 用来解决函子嵌套的问题

  • 一个函子如果具有join和of两个方法并遵循一些定律(一些数学规律,这里不展开说)那就是Monad函子

const fp = require('lodash/fp')
// IO Monad
class IO {
    static of (x) {
        return new IO(function () {
            return x
        })
    }
    constructor(fn) {
        this._value = fn
    }
    map(fn) {
        return new IO(fp.flowRight(fn, this._value))
    }
    join() {
        return this._value()  // join 方法直接返回对_value的调用, 类似于拆包装的过程
    }			 // 因为_value里面封装的是包装好的不纯的函数,_value执行后会将这个不纯函数返回
    flatMap(fn) {			  
        return this.map(fn).join() // map后的返回的函子是嵌套的,所以需要调用join给拆开
    }
}
let r = readFile('package.json')
    .map(fp.toUpper)
    .flatMap(print)
    .join()

// 在Monad里面,我们传递的函数最终会返回出一个函子,所以我们调用this._value可以返回一个函子
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值