函数式编程建立在数学基础之上,比如函数式编程的纯函数就是数学中的纯函数,我们要学习的函子也是建立在数学中的范畴论。
为什么要学函子
我们需要在函数式编程中把副作用控制在可控范围内、异常处理、异步操作等。
什么是 Functor
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
Functor 函子
// 一个容器,包裹一个值
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)
// ouput Container { _value: 36 }
每次创建函子时,需要使用 new 关键字创建对象,我们可以使用 of 静态方法封装。
// 一个容器,包裹一个值
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)
总结
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了 map 契约的对象
- 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
- 最终 map 方法返回一个包含新值的盒子(函子)
Functor 函子存在的问题
纯函数要满足相同的输入始终要获得相同的输出。
在 Functor 中如果我们传入 null 或 undefined 时,纯函数变得不纯。
// 值如果不小心传入了空值(副作用)
Container.of(null)
.map(x => x.toUpperCase())
// TypeError: Cannot read property 'toUpperCase' of null
这种情况下,传入的 null 或 undefined 就是副作用,我们要想办法控制这个副作用。
MayBe 函子
- 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理
- MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
class Maybe {
static of (value) {
return new Maybe(value)
}
constructor (value) {
this._value = value
}
map (fn) {
// 当 value 为 null 或 undefined,不会调用 fn,而直接返回一个值为 null 的函子
return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this._value))
}
isNothing () {
return this._value === null || this._value === undefined
}
}
let r1 = Maybe.of('Hello World').map(x => x.toUpperCase())
console.log(r)
// output: Maybe { _value: 'HELLO WORLD' }
let r2 = Maybe.of(null).map(x => x.toUpperCase())
// output: Maybe { _value: null }
MayBe 函子与 Container 函子相比,需要先判断 value,当 value 为 null 或 undefined 时,不会调用 fn,而直接返回一个值为 null 的函子。
MayBe 函子虽然可以处理空值的问题,但是如果我们多次调用 map
方法时,我们无法得知是哪一次出现了空值:
let r3 = Maybe.of('hello world').map(x => x.toUpperCase).map(x => null).map(x => x.split(' '))
console.log(r3)
// output: Maybe { _value: null }
// 是在哪一次出现了 null 呢?
Either 函子
- Either 两者中的任何一个,类似于 if…else…的处理
- 异常会让函数变的不纯,Either 函子可以用来做异常处理
class Left {
static of (value) {
return new Left(value)
}
constructor (value) {
this._value = value
}
map () {
return this
}
}
class Right {
static of (value) {
return new Right(value)
}
constructor (value) {
this._value = value
}
map (fn) {
return Right.of(fn(this._value))
}
}
let r1 = Right.of(12).map(x => x + 2)
let r2 = Left.of(12).map(x => x + 2)
Either 用来处理异常
function parseJSON(json) {
try {
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 {
// IO 函子内部通过一个函数包裹了一个返回的值
static of (x) {
return new IO(function () {
return x
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
// 把当前的 value 和 传入的 fn 组合成一个新的函数
return new IO(fp.flowRight(fn, this._value))
}
}
// 调用
let result = IO.of(process).map(p => p.execPath)
console.log(result)
// 将不纯的操作延迟到调用的时候。
console.log(result._value())
Task 函子
Folktale
Task 函子用于处理异步执行,异步任务的实现过于复杂,我们使用 folktale 中的 Task 来演示。
Folktale 是一个标准的函数式编程库,提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、MayBe 等。
// Task 处理异步任务
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
// 读取文件 异步操作
function readFile(filename) {
return task(resolver => {
fs.readFile(filename, 'utf-8', (err, data) => {
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
readFile('package.json')
.run()
.listen({
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value)
}
})
对返回的值进行处理,可以使用 map 方法。
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value)
}
})
Pointed 函子
Pointed 函子是实现了 of 静态方法的函子
of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文 Context(把值放到容器中,使用 map 来处理值)
Monad 函子
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
})
}
let cat = fp.flowRight(print, readFile)
// IO(IO(x))
let r = cat('package.json')
console.log(r)
// IO { _value: [Function] }
虽然我们也可以通过 ._value()._value()
调用到值,但是这样较为麻烦。
// ...
let cat = fp.flowRight(print, readFile)
// IO(IO(x))
let r = cat('package.json')._value()._value()
console.log(r)
Monad 函子的 join 方法
Monad 单子可以帮助我们解决函数嵌套的问题。
Monad 函子是可以变扁的 Pointed 函子,IO(IO(x))。
Monad 函子具有 of 方法和 join 方法。
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()
}
// 先调用 map 再调用 join
flatMap (fn) {
return this.map(fn).join()
}
}
let r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
将获取函子的值通过方法封装到函子内部,在封装时虽然复杂,但是实际开发便轻松了。
当一个函数返回函子时,便可以使用 Monad 函子。
Monad 函子可以帮助我们解决函子嵌套问题,使得代码更具可读性。