JavaScript 深度剖析---03函数式编程范式
functor(涵子)
函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。
-
为什么要学函子
到目前为止已经已经学习了函数式编程的一些基础,但是我们还没有演示在函数式编程中如何把副作用
控制在可控的范围内、异常处理、异步操作等。 -
函子的概念
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。 -
什么是 Functor
容器:包含值和值的变形关系(这个变形关系就是函数)
函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,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);
上面代码中,Container 是一个函子,它的map方法接受函数f作为参数,然后返回一个新的函子,里面包含的值是被f处理过的(f(this.val))。
一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。
-
of 方法
可能注意到了,上面生成新的函子的时候,用了new命令。这实在太不像函数式编程了,因为new命令是面向对象编程的标志。
函数式编程一般约定,函子有一个of方法,用来生成新的容器。
下面就用of方法替换掉new。
class Container {
static of (value){
return new Container(value)
}
constructor(value) {
this._value = value
}
map(fn) {
return Container.of(fn(this._value))
}
}
let r = Container.of(5).map(x => x + 1).map(x => x * x)
console.log(r);
这就更像函数式编程了。
- 总结
- 函数式编程的运算不直接操作,而是由函子完成
- 函子就是一个实现了map契约的对象
- 我们把函子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
- 最终map方法返回一个包含新值的盒子(函子)
MayBe(函子)
- 函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
- 我们在编程的过程中可能会遇到很多错误,需要对这些错误做相对应的处理
- MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
Container.of(null).map(function (s) {
return s.toUpperCase();
});
// TypeError
上面代码中,函子里面的值是null,结果小写变成大写的时候就出错了。
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 函子就是为了解决这一类问题而设计的。简单说,它的map方法里面设置了空值检查。
// 在 MayBe 函子中,我们很难确认是哪一步产生的空值问题,如下例:
MayBe.of('hello world').map(x => x.toUpperCase()).map(x => null).map(x => x.split(' ')) // => MayBe { _value: null }
//因此引入了Either函子
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异步执行
- olktale(2.3.2) 2.x 中的 Task 和 1.0 中的 Task 区别很大,1.0 中的用法接近近我们现在演示的函子
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const {split,find} = require('lodash/fp')
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(单子)
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
})
}
let cat = fp.flowRight(print,readFile)
let r = cat('package.json')._value()._value()
console.log(r);