文章内容输出来源:拉勾教育 大前端高薪训练营
前言
我在另一篇文章 函数式编程 – 纯函数、柯里化函数 中写到,副作用会让一些函数变得不纯,那么,我们如何把副作用控制在可控的范围内呢,这就涉及到了函子的概念。
函子(Functor)
1. 什么是函子
在开始学习之前,我们先来了解什么是函子?
-
函子是一个容器,包含值和值的变形关系(即函数)。 -
函子是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系) 代码如下(示例): // 一个容器,包裹一个值
class Container {
constructor (value) {
this._value = value // 使用_表示变量私有化
}
// map方法, 传入变形关系(函数),将容器里面的每一个值,映射到另一个容器
map (fn) {
return Container.of(fn(this._value))
}
}
// 创建函子对象
let r = new Container(5)
.map(x => x + 1) // 返回新的函子对象, 在新的函子对象中保存值
.map(x => x * x )
console.log(r);
上面的代码中,Container 是一个函子,它的map方法接受函数f作为参数,然后返回一个新的函子,里面包含的值是被 fn 处理过的(fn(this._value))。 上面生成新的函子对象的时候,用了 new 命令。new 命令是面向对象编程的标志,不符合函数式编程的思想。 -
函数式编程一般约定,函子有一个of方法,用来生成新的容器。 那么,我们接下来就用 of 方法替换掉 new 进行改造。 代码如下(示例): class Container {
// of 使用static,将其设置为静态方法,可以使用 '类.类方法' 的方式调用
static of (value) {
return new Container(value)
}
...... // 下面代码和上面的一样,就不在此赘述了
}
// 链式编程
let r = Container.of(5).map(x => x + 2).map(x => x * x)
console.log(r);
-
总结 1、函数式编程的运算不直接操作值,而是由函子完成 2、函子就是一个实现了 map 契约的对象 3、我们可以把函子想象成一个盒子,这个盒子里封装了一个值 4、想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理 5、最终 map 方法返回一个包含新值的盒子(函子)
2. MayBe 函子
空值问题
解决方案
-
MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围),准确的说是,它的 map 方法里面设置了空值检查。 代码如下(示例): class Maybe {
map (fn) {
return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this._value))
}
isNothing () {
return this._value == null || this._value == undefined
}
}
// 测试
let r = Maybe.of('Hello World').map(x => x.toUpperCase()).map(x => null).map(x => x.split(' '))
console.log(r);
然而,在 MayBe 函子中,我们很难确认是哪一步产生的空值问题,要解决这个问题,我们就要借助下面的 Either 函子 ,去处理异常情况。
3. Either 函子
在普通的面向对象编程中,我们通常使用条件运算语句 if…else… 进行异常等方面的判断。而在函数式编程中,我们是用 Either 函子 进行表达。Either,英文意思,两者中的任何一个。
-
Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。 代码如下(示例): // 记录错误信息, 右值不存在时使用的默认值
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 (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
let r = parseJSON('{ 'name': 'zs' }').map(x => x.name.toUpperCase())
console.log(r) // 执行 Right
4. IO 函子
在程序运行中,往往会有很多的函数依赖于外部环境,从而会带来相应的副作用,这也就是我们前面所说的不纯函数,在这里,我们就不多加赘述了。那么,如何可以把不纯的函数,让它 “纯”起来呢?为了解决这个问题,我们需要一个新的 Functor,即 IO 函子。
特性
-
IO 函子与其他函子的不同在于,IO 函子中的 _value 是一个函数,把函数作为值来处理。 -
IO 函子可以把不纯的动作存储到 _value(函数) 中,延迟执行这个不纯的操作(惰性执行)。可以认为,IO 包含的是被包裹的操作的返回值。 -
IO 函子把不纯的操作交给调用者来处理。 代码如下(示例): const { values } = require('lodash')
const fp = require('lodash/fp')
class IO {
static of (value) { //
return new IO(function () {
return value
})
}
constructor (fn) { // value 存储函数
this._value = fn
}
map (fn) {
// 将传入的 fn 进行包裹,利用fp.flowRight() 使之柯里化
return new IO(fp.flowRight(fn, this._value))
}
}
// 调用,process:node中的进程模块
let r = IO.of(process).map(p => p.execPath)
// console.log(r) // IO { _value: [Function] }
console.log(r._value()); // 当前node进程的执行路径
5. Folktale
-
folktale 一个标准的函数式编程库,和 lodash、ramda 不同的是,他没有提供很多功能函数。 -
只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、 MayBe 等。 代码如下(示例): // Folktale 函数式编程库
const { toUpper, first } = require('lodash/fp')
const { compose, curry } = require('folktale/core/lambda')
// 第一个参数是传入函数的参数个数
let f = curry(2, (x, y) => x + y)
console.log(f(1, 2));
console.log(f(1)(2));
let f = compose(toUpper, first)
console.log(f(['one', 'two']));
-
Task 异步执行 Task 函子通过类似 Promise 的 resolve 的风格来声明一个异步流程,在下面的代码中声明的 readFile 函数中返回的 Task 函子 并没有真正发起请求,它只声明了一个请求动作,这个动作并没有被执行。 代码如下(示例): const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')
function readFile (filename) {
// 通过类似 Promise 的 resolve 的风格来声明一个异步流程,返回一个Task 函子
return task(resolver => {
// fs 的readFile() 执行的是异步操作
fs.readFile(filename, 'utf-8', (err, data) => {
// 类似Promise中的resolve 和 reject
// reject用来报错误信息,resolve用来获取执行成功的数据。
if (err) resolver.reject(err)
resolver.resolve(data)
})
})
}
let version = readFile('../package.json') // 只声明读取文件的动作,该动作并未执行
.map(split('\n')) // 通过 map 方法,添加不同的数据操作流程。
.map(find(x => x.includes('version'))) // includes() 方法用于判断字符串是否包含指定的子字符串。
.run() // 调用 run() 触发上面的动作,进行
.listen({
onRejected: err => { // 执行失败
console.log(err);
},
onResolved: value => { // 执行成功
console.log(value);
}
})
console.log(version); // 'version': '1.0.0'
在上面的代码中,Task 的异步流直到 run 之前都仅仅是「动作」,没有「执行」。task 函子中提供了 run() 方法,用来触发动作的执行。也就是说,执行 run 方法之后,才会触发上面的文件读取,以及对文件内容的一系列处理等操作。Task 函子中,还提供了 listen() 方法,用来监听事件的执行状态。onRejected 表示 动作执行失败后,要执行的函数,onResolved 表示 动作执行成功后,要执行的函数。
6. Pointed 函子
-
Pointed 函子是实现了 of 静态方法的函子; -
of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文 Context(把值放到容器中,使用 map 来处理值) 代码如下(示例): class Container {
static of (value) {
return new Container(value)
}
......
}
Contanier.of(2) .map(x => x + 5)
7. 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))
// 调用 _value() 时,执行的是print 中的function
let cat = fp.flowRight(print, readFile)
// 调用
let r = cat('package.json')._value()._value()
console.log(r)
特性
-
Monad 函子是可以变扁的 Pointed 函子,IO(IO(x)) -
Monad 内部封装的值是一个函数(这个函数返回函子) -
一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad 代码如下(示例): // IO Monad
const fs = require('fs')
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))
}
// 通过 join 方法避免函子嵌套
join () {
return this._value()
}
// 同时调用map 和 join
flatMap (fn) {
// this.map(fn) 调用完后,返回函子
return this.map(fn).join()
}
}
// 读取文件的内容,并且把他们打印出来
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 r = readFile('../package.json') // 返回函子时,调用faltMap; 返回值时,调用map
// .map(x => x.toUpperCase())
.map(fp.toUpper)
.flatMap(print) // 返回 IO { _value: [Function] } -- 函子
.join() // 返回 map 后的文件内容
console.log(r);
作用
- Monad 函子 主要用来解决函子嵌套的问题,通过 join 方法避免函子嵌套。
何时使用
- 当一个函数返回一个函子的时候,需要使用 Monad。
总结
- 简单说,Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算所需的函数,整个运算就会自动进行下去。也就是说,Monad 是将一个会返回包裹值的函数应用到一个被包裹的值上。
参考 【函数式编程入门教程】 【异步流程与 Task 函子】 【JavaScript函数式编程 IO涵子,错误处理涵子】
|