到目前为止已经学习了函数式编程的一些基础,比如说纯函数、柯里化、函数组合,函数式编程其实就是把一些运算抽象为函数,将来这些函数我们可以做到最大化的重用,另外我们还知道,我们的函数式编程是建立在数学思想上的,比如我们的纯函数就是我们数学中的函数,我们现在要学习的函子,其实也是建立在数学的基础上的,但是我们还没有演示在函数式编程中如何把副作用控制在可控范围内、异常处理、异步操作等,而这些我们之后可以使用函子来控制。
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)
/**
* 函子是一个普通的对象,这个对象里边应该维护一个值,并且要对外公布一个map方法
* 所以我们可以通过一个类来描述函子
*
* 1. 我们先定义一个类,类名Container也只是因为函子是一个容器所以这样比较见名知意而已
*
* 2. 我们要先在这个类中定义一个构造函数constructor
* 但我们创建函子时,函子内部要有一个值,所以在构造函数中我们要把这个值传递进来,函子内部把这个值存储起来
* 注意这个值是函子内部维护的,只有他自己知道,这个值是包含在一个盒子里面的,它是不对外公布的
*
* 3. 所以定义的时候我们用this._value,我们要提前约定,所有以_开头的属性都是私有属性
* 也就是说这个属性我们不想让外部去访问,他的值就是我们在初始化的时候传递进来的value
*
* 4. 接下来我们这个盒子要对外公布一个map方法,map方法的作用是接收一个处理值的函数
* 这个函数也是一个纯函数,因为我们要把value传递给这个函数,由这个函数去真正处理我们这个值
*
* 5. 在这个函数中我们要去处理这个值,并且返回一个新的盒子,也就是返回一个新的函子
*
* 6. 再返回新的函子时我们要把处理的值传递给这个Container
*
* 这就完成了一个基本的函子
*
* 我们现在在来思考一个问题
*
* 我们每次要创建一个函子的时候,我们都要去调用new来处理,这样有些不方便,所以我们可以把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 + 2)
.map(x => x * x)
console.log(r._value)
/**
* 我们每次调用Container的时候都需要使用New,看起来很面向对象,因为都需要new,
* 而现在我们用的不是面向对象的思想,我们用的是函数式编程,所以我们想避免这个new
*
* 那怎么避免呢?
*
* 我们可以在 Container 创建一个静态的方法是 static,方法名字叫做 of
* of 方法的作用就是给我们返回一个函子对象,但是我们在创建时要传入一个 value,所以 of 就去接受一个 value
* 然后在 of 方法内直接去 return 一个 new Container 并且将value传入进去
*
* 然后在map方法内将 new Container改善成Container.of
* */
// 调用Container.of()并传递一个参数时,不小心传递一个null或者undefind,这时候会发生什么问题呢?
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(null)
.map(x => x.toUpperCase())
console.log(r)
/**
* 因为我们传递的是null,而在 map 方法里边我们要去执行 null.toUpperCase()
* 所以这里会出现空值得异常 --- TypeError: Cannot read property 'toUpperCase' of null
*
* 并且会让我们这个函数不纯,因为纯函数是对相同的输入始终有相同的输出
* 而我们输入null的时候,此时这个函数没有输出,而是出现了异常
*
* 那这个时候我们传入的null,其实就是我们的副作用,接下来我们需要解决这个问题,去控制这个副作用
*
* 下一小节我们会通过一个新函子,去处理空值的问题
* */
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' }
// 传入一个null或者undefined时
// let r = MayBe.of(null)
// .map(x => x.toUpperCase())
// console.log(r) // MayBe { _value: null }
// 多次调用的中间传入一个null或者undefined时
let r = MayBe.of('hello')
.map(x => x.toUpperCase())
.map(x => null)
.map(x => x.split(' '))
console.log(r) // MayBe { _value: null }
// 不知道什么时候变为Null的
Either 两者中的任何一个,类似于 if…else… 的处理
异常会让函数变得不纯,Either 函子可以用来做异常处理
示例
const {
truncate } = require("fs")
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))
}
}
// let r1 = Right.of(12).map(x => x + 2)
// let r2 = Left.of(12).map(x => x + 2)
// console.log(r1) // Right { _value: 14 }
// console.log(r2) // Left { _value: 12 }
/**
* 我们需要先定义两个类,两个类之间有区别,主要是Map方法中有所区别
*
* 我们打印后的两个输出的结果是不一样的,两个代码都是一样的,只是一个使用left创建,一个试用right创建
*
* right中的值 +2 返回了14,但是 left 中直接是返回了传入的数据,没有做任何处理
*
* 我们可以对比两个类中的map方法
*
* Left 中 直接把当前对象返回了,并没有调用我们传入的fn,这样做是为了让Left中嵌入一个错误消息
*
* 下边我们来掩饰一个可能会发生错误的函数,比如我们要把一个json形式的字符串,转成一个json对象
* */
function parseJSON (str) {
try{
return Right.of(JSON.parse(str))
} catch (e) {
return Left.of({
error : e.message})
}
}
// let r1 = parseJSON('{ name: zs }')
// console.log(r1) // Left { _value: { error: 'Unexpected token n in JSON at position 2' } }
// let r2 = parseJSON('{ "name": "zs" }')
// console.log(r2) // Right { _value: { name: 'zs' } }
let r3 = parseJSON('{ "name": "zs" }')
.map(x => x.name.toUpperCase())
console.log(r3) // Right { _value: 'ZS' }
/**
* 我们先声明一个叫做 parseJSON 的函数,并给他接收一个参数
*
* 接下来我们要调用JSON.parse把我们传入的字符串转为json对象并且返回
*
* 因为调用JSON.parse时可能会出现异常,所以我们使用 try {} catch(e) {}
*
* 因为出现错误情况我们不去处理的话,那就不是一个纯函数
* 现在我们希望用函数式的方式去处理,所以我们需要写一个纯函数
*
* 我们现在要在try里return一个函子,我们会把我们转换后的结果交给这个函子,将来在这个函子内部去处理
*
* 如果出现错误我们是不能不管的,我们也要返回一个值,因为对于纯函数来说,对于相同的输入,始终要有相同的输出
*
* 那这个时候我们也要返回一个函子,因为我们Either中有Left和Right,我们用right去处理正确的值
* 如果出现异常的时候我们可以返回一个left中的函子,而left这个函子里边,可以帮我们去存储一些错误的信息
* */
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( x => x.execPath)
console.log(r._value()) // C:\Program Files\nodejs\node.exe
/**
* constructor要去接收一个函数,因为IO函子里边保存的是函数,我们将接收到的函数保存在 _value 中
*
* of方法与之前有点差异,他要接受一个数据,然后返回一个新的IO函子
*
* 在这里便要返回一个IO函子,所以要调用IO构造函数,然后传入一个函数
* 因为刚刚我们写过它的构造函数,它的构造函数需要接收的是一个函数
*
* 所以在这里我们传入一个函数,在这个函数内,我们把刚刚of方法的值返回
*
* 到这里我们其实可以感受到IO函子,最终想要的还是一个结果,只不过他把取值的过程包装到了函数里边来
* 将来需要值的时候,在来执行这个函数,来取值
*
* map方法和之前一样还是需要传递一个fn,这个方法里边我们还是要返回一个IO函子,这里我们要调用IO的构造函数
* 而不是调用of方法,因为外部方法里面,我们把当前函子的value,也就是这个函数和我们传入的这个函数
* 组合成一个新的函数,而不是调用函数去给值 --- 这就是和以前不一样的地方
* */
函子可以帮我们控制副作用,进行异常处理,还可以帮我们处理异步任务,因为在异步操作中,会出现回调地狱而使用 Task 函子,会避免回调地狱。
const {
compose, curry } = require('folktale/core/lambda')
const {
toUpper, first } = require('lodash/fp')
// 这里的柯里化,与lodash中的柯里化,稍微有一点点的区别
let f = curry(2, (x, y) => x + y)
/**
* 两个参数:
* 第一个:指明我们后边这个函数他有几个参数,目的是为了避免一些错误
* */
console.log(f(1, 2))
console.log(f(1)(2))
console.log(f()(1, 2))
// compose --- 函数组合 --- 和lodash中的函数组合 flowRight 不一样
let f = compose(toUpper, first)
console.log(f(['one', 'two']))
// 读取package.json,并将其中的version解析出来
const {
task } = require('folktale/concurrency/task') // 导入task函子,路径可查官网
const fs = require('fs') // 导入fs模块
const {
split, find } = require('lodash/fp') // 导入lodash/fp的方法
// 读取文件的函数,需要一个参数 --- 文件的路径,相对路径可以直接写文件名'package.json'
function readFile ( filename ) {
/**
* 返回一个task函子,需要接受一个函数,函数的参数是固定的resolver,可以通过官网来查询
* resolver是一个对象,里边提供了两个方法:resolve() 和 reject() --- 类似于promise
* */
return task(resolver => {
// 异步读取文件,需要三个参数:1---读取的路径;2---使用的编码;3---回调函数(在node中是错误优先);
fs.readFile(filename, 'utf-8', (err, data) => {
// 判断读取文件时是否出错
if(err){
// 失败时调用
resolver.reject(err)
}
// 成功时调用
resolver.resolve(data)
})
})
}
// 传入路径
readFile('package.json')
/**
* 执行完readFile他不会去读取文件,而是返回了一个Task的函子
* 我们想要它读取文件的话需要调用Task给我们提供的.run方法
* */
.run()
/**
* 读取完文件,我们没有传递resolve,我们不知道如何去处理这个数据
* 所以我们需要Task给我们提供的listen来监听当前的执行状态,这里是以时间的方式来给我们提供的
* */
.listen({
// 失败时执行的
onRejecter: err => {
console.log(err)
},
// 成功是执行的
onResolved: value =>{
console.log(value)
}
})
// 以换行为切割,分成数组,然后再去其中寻找version
readFile('package.json')
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
.listen({
onRejecter: err => {
console.log(err)
},
onResolved: value =>{
console.log(value)
}
})
class Content {
static of (value) {
return new Content(value)
}
......
}
let c = Content.of(2)
.map(x => x + 3)
console.log(c)
const fp = require('lodash/fp')
const fs = require('fs')
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 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)
/**
* 结构:
* IO(IO(x))
*
* 外层IO是print返回的这个函子
* 内层IO是readFile返回的函子
*
* 因为先调用readFile这个函子,我们把readFile返回的这个函子传递给了print这个函数
*
* 所以print里边new IO内部输出的x其实就是readFile返回的这个函子,所以我们现在拿到的结果是一个函子嵌套了一个函子
* */
let r = cat('package.json')
// console.log(r) // IO { _value: [Function] }
// console.log(r._value()) // IO { _value: [Function] } IO { _value: [Function] }
console.log(r._value()._value()) // IO { _value: [Function] } { ... 打印出了package.json ...}
r._value()._value()
这样确实是可以实现,但是看起来确实不太合适Monad(单子)
了const fp = require('lodash/fp')
const fs = require('fs')
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 () {
return this._value()
}
/**
* 我们在使用monad的时候,经常会把,join和map联合起来使用
* 因为map的作用是把我们当前这个函数和我们函子内部的_value组合起来,返回一个新的函子
*
* map在组合这个函数的时候这个函数最终也会返回一个函子,所以我们需要调用Join将其变扁
*
* flatMap的作用就是同时调用map和join
* */
flatMap (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')
.flatMap(print)
.join()
/**
* 调用readFile的时候会返回一个函子,函子里边包裹了我们读取文件的操作
*
* 我们需要将读文件的操作和打印的操作合并起来
*
* 也就是接下来要将print调用进来
*
* 调用 map 还是 flatMap 取决于当我们要合并的这个函数
* 返回的直接是值就用map,返回的是一个函子我们就需要使用flatMap了
* */
console.log(r)