函子概念
- 是一个特殊的容器,通过一个普通的对象来实现,该对象具有 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可以返回一个函子