本文目录:
1.什么是函子
2.MayBe函子
3.Either函子
4.Pointed函子
5.IO函子
6.IO函子存在的问题
7.Monad函子(单子)
1.什么是函子
函子是一个包含值和值的变形关系(这个变形关系就是函数)的特殊容器,函子通过一个普通的对象来实现,该对象具有map方法,map可以运行一个函数对值进行处理(变形关系),并最终返回一个包含了新值的新函子。
看下面这段代码
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 + 2)
.map(x => x * x)
console.log(r)
在node下运行代码,在控制台上输出
Container { _value: 49 }
代码解析:
首先明确一点,该代码返回的是一个名字叫Container的函子,这个名字是我们自己定义的。
函子的里面维护了一个值,这个值不对外公布,以_开头代表私有,另外对外公布一个map方法,map方法接收一个处理值的函数,当我们调用map方法的时候,去调用fn去处理值,并且把处理后的结果传递给新的函子进行保存。
of是Container函子的静态方法,目的就是为了让Container在使用的时候不用再频繁的写new,因为new的出现会让代码显得非常的面向对象。
2.MayBe函子
上面的Container函子如果在调用of,也就是new的时候传入的value是null,会造成函数执行异常,这是一个典型的副作用,我们要想办法去规避。
MayBe函子的作用就是在内部增加了一个处理机制,对异常的数据进行判定,从而控制副作用在允许的范围内。
下面是一个MayBe函子的代码,注意,这个MayBe的名字同样是可以自定义的。
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor(value) {
this._value = value
}
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())
console.log(r)
运行代码,输出结果为
MayBe { _value: 'HELLO WORLD' }
前面有说过,map方法最终返回的是一个包含新值的新函子,所以可以进行链式调用
let r = MayBe.of('Hello World').map(x => x.toUpperCase()).map(x => null).map(x => x.split(' '))
输出结果为
MayBe { _value: null }
虽然其中某一个环节的传参有误,代码也并没有报错,实际情况的传参当然不会想演示代码这么简单,这导致了我们却没法去准确的判断是哪个环节出了错。
3.Either函子
Either的意思就是指两者中的任何一个,类似于if...else的处理。
异常会让函数变得不纯,所以我们可以用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))
}
}
我们再封装一个函数parseJSON用来进行异常的捕获
function parseJSON(str) {
try {
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({
error: e.message
})
}
}
接下来我们调用parseJSON,并其中一个故意传入错误的json数据,另外一个传入正确的
let l = parseJSON(`{name:zs}`)
console.log(l)
let r = parseJSON('{"name":"zs"}')
console.log(r)
打印结果如下
Left {
_value: { error: 'Unexpected token n in JSON at position 1' } }
Right { _value: { name: 'zs' } }
对于r,我们当然还可以调用map,对传入的数据进行处理。
let r = parseJSON('{"name":"zs"}').map(x => x.name.toUpperCase())
打印结果为
Right { _value: 'ZS' }
l因为在parseJSON的时候,异常就被捕获了,并且将错误信息进行了返回,所以后面自然不会再进行任何操作。
4.Pointed函子
Pointed函子是实现了of静态方法的函子。
of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文context中(把值放到容器中,使用map来处理值)
5.IO函子
IO函子的_value是一个函数,这里是把函数作为值来处理。
IO函子可以把不纯的动作存储到_value中,并延迟执行这个不纯的操作(惰性执行),把不纯的操作交由调用者来处理。
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))
}
}
调用这个函子
let r = IO.of(process).map(p => p.execPath)
此时打印结果,r也是一个函子,但区别是_value里面存储的是一个函数,我们需要进一步调用才能获得_value的值。
console.log(r) //IO { _value: [Function] }
console.log(r._value()) // C:\Program Files\node.js\node.exe
6.IO函子存在的问题
首先需要引入node的fs模块和lodash的fp模块
const fs = require('fs')
const fp = require('lodash/fp')
然后封装一个IO函子,下面的函数其实不能叫函子了,因为经过readFile和print的进一步封装,of和map方法都没有起到作用,把它们都注释掉也不会影响代码的执行。
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))
// }
}
接下来我们封装两个函数,分别用来读取文件和打印读取到的内容。
var readfile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8')
})
}
var print = function (x) {
return new IO(function () {
console.log(x)
return x
})
}
我们组合这两个函数,并进行调用
let cat = fp.flowRight(print, readfile)
let r = cat('package.json')
console.log(r)
此时的打印结果为
IO { _value: [Function] }
Function实际上是 IO(IO(x)) ,是一个嵌套的函子,这个函子的外层是print返回的函子,里面的是readFile返回的函子。此时函数真正的代码并没有去执行,只有进一步的调用才会去真正的执行读取文件和打印代码的操作。
let r = cat('package.json')._value()._value() 这样运行代码,在控制台中就会读取并打印出文件代码。
每次都需要这样调用太过于麻烦,所以接下来要说的是Monad函子
7.Monad函子(单子)
上面的readfile和print函数在调用的时候就相当于调用了Pointed函子的of静态方法,因为对于普通函子而言,of接收的参数是普通的值value,IO函子的of接收的是一个带有返回值的函数fn,而经过readfile和print函数的封装,此时相当于调用IO函子的of静态方法时传入的参数是一个不是一个普通函数,而是一个IO函子,在接下来通过fp.flowRight(print, readfile)进行函数组合的时候,结果自然就形成了函子的嵌套关系。
如果函数嵌套的话,我们可以使用函数组合去解决,如果函子嵌套的话,我们使用Monad函子解决。
我们在使用monad时候,经常会把map和join联合起来去使用,因为map的作用就是当前的函数和函子内部的value组合起来,返回一个新的函子,map在组合函数的时候,这个函数最终也会返回一个函子,这时候需要调用join把它变扁和拍平,而flatMap的作用就是同时去调用map和jion。
一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad函子。
上面的IO函子我们可以变成下面这样
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()
}
}
调用的时候不需要再去组合函数cat,也不再需要进行多层的_value()的调用最终执行代码。
let r = readfile('package.json').flatMap(print).join()
console.log(r)
上面代码直接就可以将读取到的文件代码打印在控制台上。
网友评论