为什么要学函子
到目前为止已经已经学习了函数式编程的一些基础,但是我们还没有演示在函数式编程中如何把副作用 控制在可控的范围内、异常处理、异步操作等
什么是 Functor
容器:包含值和值的变形关系(这个变形关系就是函数) 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运 行一个函数对值进行处理(变形关系)
# Functor 函子
// 一个容器,包裹一个值
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 中如果我们传入 null 或 undefined
// 值如果不小心传入了空值(副作用)
Container.of(null)
.map(x => x.toUpperCase())
// TypeError: Cannot read property 'toUpperCase' of null
# MayBe 函子
我们在编程的过程中可能会遇到很多错误,需要对这些错误做相应的处理 MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
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 }
在 MayBe 函子中,我们很难确认是哪一步产生的空值问题,如下例:
MayBe.of('hello world')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
// => MayBe { _value: null }
# Either 函子
Either 两者中的任何一个,类似于 if...else...的处理 异常会让函数变的不纯,Either 函子可以用来做异常处理
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))
}
}
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 {
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 io = IO.of(process).map(p => p.execPath)
console.log(io._value())
Task 异步执行
异步任务的实现过于复杂,我们使用 folktale 中的 Task 来演示 folktale 一个标准的函数式编程库 和 lodash、ramda 不同的是,他没有提供很多功能函数 只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、 MayBe 等
const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')
// 第一个参数是传入函数的参数个数
let f = curry(2, function (x, y) {
console.log(x + y)
})
f(3, 4)
f(3)(4)
// 函数组合
let f = compose(toUpper, first)
f(['one', 'two'])
Task 异步执行 folktale(2.3.2) 2.x 中的 Task 和 1.0 中的 Task 区别很大,1.0 中的用法更接近我们现在演示的 函子 这里以 2.3.2 来演示
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)
})
})
}
// 调用 run 执行
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 来处理值)
class Container {
static of (value) {
return new Container(value)
}
……
}
Contanier.of(2)
.map(x => x + 5)
Monad(单子)
在使用 IO 函子的时候,如果我们写出如下代码:
const fs = require('fs')
const fp = require('lodash/fp')
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
})
}
// IO(IO(x))
let cat = fp.flowRight(print, readFile)
// 调用
let r = cat('package.json')._value()._value()
console.log(r)
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()
}
flatMap (fn) {
return this.map(fn).join()
}
}
let r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()