到目前位置我们已经学习了函数式编程的一些基础,但是我们还没有演示在函数式编程中如何把副作用控制在可控的范围内、异常处理、异步操作等。
什么是Functor
// Functor 函子
// 函子是一个普通的对象,这个对象里维护一个私有的值,并且对外公布一个map方法。所以可以使用一个类来描述一个函子。
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 方法最终返回的不是值,而是一个新的函子对象,在这个新的函子对象里面去保存新的值,这个值始终不对外公布,想要处理这个值只能通过调用这个函子的map方法去调用我们传入的处理这个数据的函数。
每次创建函子的时候都要 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) // Container { _value: 36 }
有多少运算就有多少函子,可以使用不用的汉字来解决不同的问题。
// 演示null undefined 的问题
Container.of(null)
.map(x => x.toUpperCase())
这段代码会报错,这就说明传入的这个函数并不是一个纯函数,因为纯函数对于不同的输入始终会有不同的输出,而输入null的时候,就没有输出,而是报错。这个 null 就是副作用。如何解决这个副作用呢?
MayBe ,可能会是……
// MayBe 函子
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor (value) {
this._value = value
}
isNothing () {
return (this._value === undefined || this._value === null)
}
map (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
}
// let r = MayBe.of("hello world")
// .map(x => x.toUpperCase())
// console.log(r)
// let r = MayBe.of(undefined)
// .map(x => x.toUpperCase())
// console.log(r) // MayBe { _value: null }
let r = MayBe.of(undefined)
.map(x => x.toUpperCase())
.map(null)
.map(x => split(' '))
console.log(r) // MayBe { _value: null }
上述代码中解决了传入的值为 null 或者 undefined 的副作用,但是,在最后的调用中,最后返回的函子对象中的值为 null ,但是我们链式操作了三步,就不知道这个 null 是在那一步中产生的。怎么解决呢?
// 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))
}
}
function parseJSON (str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({ error: e.message })
}
}
// let r = parseJSON('{ name: zs }')
// console.log(r) // Left { _value: { error: 'Unexpected token n in JSON at position 1' } }
let r = parseJSON('{ "name": "zs" }')
.map(x => x.name.toUpperCase())
console.log(r) // Right { _value: 'ZS' }
// IO 函子
const fp = require('lodash/fp')
class IO {
static of (value) {
return new IO(function () {
return value
})
}
constructor (fn) {
this._value = fn
}
map (fn) {
return new IO(fp.flowRight(fn, this._value))
}
}
// 调用
let r = IO.of(process).map(p => p.execPath)
console.log(r, r._value()) // IO { _value: [Function] } D:\App\nodejs\node.exe
IO 函子就是将普通的函子中的 _value 改成了一个返回 “_value” 的函数,当给函子传递一个函数的时候,就算这个函数是一个不纯的操作,它最终返回的新的函子的 _value 也会是一个固定的函数,这样就满足了对于不同的输入就会有不同的输出,且不会出现异常,就算是出现异常也是在延迟到后面的操作出现的,这样就对某些不纯的操作(副作用)进行了有效的控制。
本质上,就是把map中传递的有可能不纯的操作延迟到 _value 这个函数的时候执行的时候。从而保证函子的副作用在可控的范围内发生。
函子可以帮我们控制副作用,进行异常处理,还可以去处理异步任务,因为在异步任务中会出现地狱回调使用 task 函子可以避免回调地狱。
Task 异步执行
folktale 一个标准的函数式编程库
//**********************************************folktale */
const {compose, curry} = require('folktale/core/lambda')
const {toUpper, first} = require('lodash/fp')
let fn = compose(toUpper, first)
console.log(fn('aaa', 'bbb', 'ccc'))
let f = curry(3, (a, b, c) => {
return a + b + c
})
console.log(f(1)(2)(3))
// *****************************************task
// task 处理异步任务
const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
function readFile (url) {
return task(resolver => {
fs.readFile(url, 'utf-8', (err, data) => {
if(err) resolver.reject(err)
resolver.resolve(data)
})
})
}
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejected: err => {
console.log(err)
},
onResolved: value => {
console.log(value)
}
})
如果要获取文件中数据的 version 字段,可以直接在这个onResolved回调中处理数据,但是这样并不符合函数式编程。