为什么要使用函数组合?
因为纯函数和柯里化会很容易就形成洋葱代码 ( 多个括号嵌套 )
比如: 获取数组中最后一个元素 并且把它转化为大写
_.toUpper(_.first(_.reverse(array)))
函数组合可以把细粒度的函数重新组合成一个新的函数
程序使用函数处理数据的过程可以看做是一个管道
比如: 数据a ——> 通过函数fn ——> 得到结果b
当fn比较复杂的时候,我们可以把fn拆分成多个小函数
比如: 数据a ——> 通过f1——>得到m——> 通过f2——> 得到b
类似下面这种代码
fn = compose(f1,f2,f2)
b = fn(a)
// 函数组合演示
function compose(f, g) {
return function (value) {
return f(g(value))
}
}
// 数组翻转函数
function reverse (array) {
return array.reverse()
}
// 获取函数第一个元素函数
function first (array) {
return array[0]
}
// 组合函数,获取函数最后一个元素
const last = compose(first, reverse)
console.log(last([1, 2, 3, 4])) // 4
lodash 中组合函数 flow() 或者flowRight()
const _ = require( "lodash" )
const reverse = arr=>arr.reverse()
const first = arr=>arr[0]
const toUpper = str => str.toUpperCase()
const fn = _.flowRight(toUpper, first, reverse)
console.log(fn(['tom','jerry','jim','lucy'])) //LUCY
function compose(...args){
return function(value){
// args是要依次执行的函数数组,要从右到左执行,所以需要先reverse
// reduce 是数组方法,第一个参数是一个回调,回调必须填入两个形参,
// 形参1 表示初始值或者回调函数计算后的值
// 形参2 表示当前元素
// 比如 [1,2,3].reduce((a,b)=> a+b ) //第一次执行 a就是1,b就是2 第二次执行 a是3 b是3
// reduce第二个参数 是用于指定初始值 也就是指定回调的第一个形参的初始值
// 我们设置为value 这样第一次执行的时候 result就是value initFn就是args中的第一个成员函数
return args.reverse().reduce((result,initFn)=>{
return initFn(result)
},value)
}
}
// 可以通过箭头函数简化代码
const compose = (...args) => value => args.reverse().reduce((result,initFn)=>initFn(result),value)
函数的组合要满足结合律
比如 a 和 b 组合 再跟 c组合 等效与 b先跟c组合 再跟a组合
(A,B),C 与 A,(B,C) 等效
在函数组合的管道中,每一个细粒度的函数只能接收一个参数即上一个函数的执行结果。所以如果某一个函数需要多个参数 那么我们需要对其做柯里化处理
我们采取定义一个打印函数用来调试
// NEVER SAY DIE --> nerver-say-die
const _ = require('lodash')
// 对需要多个参数的函数进行柯里化
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, array) => _.join(array, sep))
// 我们需要对中间值进行打印,并且知道其位置,用柯里化输出一下
const log = _.curry((tag, v) => {
console.log(tag, v)
return v
})
// 从右往左在每个函数后面加一个log,并且传入tag的值,就可以知道每次结果输出的是什么
const f = _.flowRight(join('-'), log('after toLower:'), _.toLower, log('after split:'), split(' '))
// 从右到左
//第一个log:after split: [ 'NEVER', 'SAY', 'DIE' ] 正确
//第二个log: after toLower: never,say,die 转化成小写字母的时候,同时转成了字符串,这里出了问题
console.log(f('NEVER SAY DIE')) //n-e-v-e-r-,-s-a-y-,-d-i-e
// 修改方式,利用数组的map方法,遍历数组的每个元素让其变成小写
// 这里的map需要两个参数,第一个是数组,第二个是回调函数,需要柯里化
const map = _.curry((fn, array) => _.map(array, fn))
const f1 = _.flowRight(join('-'), map(_.toLower), split(' '))
console.log(f1('NEVER SAY DIE')) // never-say-die
因为函数组合中函数只能接收一个参数,这样需要对已有的函数做大量柯里化的工作,我们可以使用lodash的fp模块提供的一些函数
// lodash 模块
const _ = require('lodash')
// 数据置先,函数置后
_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
_.map(['a', 'b', 'c'])
// => ['a', 'b', 'c']
// 数据置先,规则置后
_.split('Hello World', ' ')
//BUT
// lodash/fp 模块
const fp = require('lodash/fp')
// 函数置先,数据置后
fp.map(fp.toUpper, ['a', 'b', 'c'])
fp.map(fp.toUpper)(['a', 'b', 'c'])
// 规则置先,数据置后
fp.split(' ', 'Hello World')
fp.split(' ')('Hello World')
一种编程风格,就是上面的函数组合。
Point Free: 我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
// world wild web ---> World.Wild.Web
// 先转成数组
// 再把首字母换成大写
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(fp.join('.'),fp.map(fp.upperFirst),fp.split(' '))
console.log(firstLetterToUpper('world wild web'));
函子( representative functor ) 是范畴论里的概念,我们没有办法避免副作用,但是可以通过函子让副作用控制在可控范围内,同时也可以通过函子处理异常,异步等
class Container() {
constructor(value){
this._value = value // 加_表示永远不暴露该属性
}
map(fn){
// 调用value的变形关系 fn
// 返回一个新的函子实例 其中的_value就是上一次fn运算的结果
return new Container(fn(value))
}
}
因为还是有new的存在是面向对象思想 所以我们修改为函数式编程
class Container() {
static of(value){
return new Container(value)
}
constructor(value){
this._value = value // 加_表示永远不暴露该属性
}
map(fn){
// 调用value的变形关系 fn
// 返回一个新的函子实例 其中的_value就是上一次fn运算的结果
return Container.of(fn(value))
}
}
可以对外部的空值情况做处理(控制副作用在允许的范围)
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor (value) {
this._value = value
}
map(fn) {
// 判断一下value的值是不是null和undefined,如果是就返回一个value为null的函子,如果不是就执行函数
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
}
// 定义一个判断是不是null或者undefined的函数,返回true/false
isNothing() {
return this._value === null || this._value === undefined
}
}
const r = MayBe.of('hello world')
.map(x => x.toUpperCase())
console.log(r) //MayBe { _value: 'HELLO WORLD' }
// 如果输入的是null,是不会报错的
const rnull = MayBe.of(null)
.map(x => x.toUpperCase())
console.log(rnull) //MayBe { _value: null }
// 因为是二选一,所以要定义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))
}
}
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 }
// 为什么结果会不一样?因为Left返回的是当前对象,并没有使用fn函数
// 那么这里如何处理异常呢?
// 我们定义一个字符串转换成对象的函数
function parseJSON(str) {
// 对于可能出错的环节使用try-catch
// 正常情况使用Right函子
try{
return Right.of(JSON.parse(str))
}catch (e) {
// 错误之后使用Left函子,并返回错误信息
return Left.of({
error: e.message })
}
}
let rE = parseJSON('{name:xm}')
console.log(rE) // Left { _value: { error: 'Unexpected token n in JSON at position 1' } }
let rR = parseJSON('{"name":"xm"}')
console.log(rR) // Right { _value: { name: 'xm' } }
console.log(rR.map(x => x.name.toUpperCase())) // Right { _value: 'XM' }
const fp = require('lodash/fp')
class IO {
// of方法快速创建IO,要一个值返回一个函数,将来需要值的时候再调用函数
static of(value) {
return new IO(() => value)
}
// 传入的是一个函数
constructor (fn) {
this._value = fn
}
map(fn) {
// 这里用的是new一个新的构造函数,是为了把当前_value的函数和map传入的fn进行组合成新的函数
return new IO(fp.flowRight(fn, this._value))
}
}
// node执行环境可以传一个process对象(进程)
// 调用of的时候把当前取值的过程包装到函数里面,再在需要的时候再获取process
const r = IO.of(process)
// map需要传入一个函数,函数需要接收一个参数,这个参数就是of中传递的参数process
// 返回一下process中的execPath属性即当前node进程的执行路径
.map(p => p.execPath)
console.log(r) // IO { _value: [Function] }
// 上面只是组合函数,如果需要调用就执行下面
console.log(r._value()) // C:\Program Files\nodejs\node.exe
const {
task } = require('folktale/concurrency/task')
const fs = require('fs')
function readFile (filename) {
// task传递一个函数,参数是resolver
// resolver里面有两个参数,一个是reject失败的时候执行的,一个是resolve成功的时候执行的
return task(resolver => {
//node中读取文件,第一个参数是路径,第二个是编码,第三个是回调,错误在先
fs.readFile(filename, 'utf-8', (err, data) => {
if(err) resolver.reject(err)
resolver.resolve(data)
})
})
}
// readFile调用返回的是Task函子,调用要用run方法
readFile('package.json')
//在run之前调用map方法,在map方法中会处理的拿到文件返回结果
.map(split('\n'))
.map(find(x => x.includes('version')))
.run()
// 现在没有对resolve进行处理,可以使用task的listen去监听获取的结果
// listen传一个对象,onRejected是监听错误结果,onResolved是监听正确结果
.listen({
onRejected: (err) => {
console.log(err)
},
onResolved: (value) => {
console.log(value)
}
})
函子是一个容器,可以包含任何值。函子之中再包含一个函子,也可以。但是,这样就会出现多层嵌套的函子。
Monad 函子的作用是,总是返回一个单层的函子
Monad 主要通过 join 和 flatMap两个方法实现解决函子嵌套的问题。
const fp = require('lodash/fp')
const fs = require('fs')
class IO {
static of (value) {
return new IO(() => {
return value
})
}
constructor (fn) {
this._value = fn
}
map(fn) {
return new IO(fp.flowRight(fn, this._value))
}
join () {
return this._value()
}
// 同时调用map和join方法
flatMap (fn) {
return this.map(fn).join()
}
}
let readFile = (filename) => {
return new IO(() => {
return fs.readFileSync(filename, 'utf-8')
})
}
let print = (x) => {
return new IO(()=> {
console.log(x)
return x
})
}
let r = readFile('package.json') // 得到一个_value值为 fs.readFileSync(filename, 'utf-8') 的IO函子
// return this.map(fn--就是print函数)
//也就是把上面的文件读取函数执行结果作为x传入给print函数
//print函数.join() 其实就是执行 new IO(()=> {console.log(x); return x})
.flatMap(print)
// 执行 ()=> {console.log(x); return x }
.join()
r = readFile('package.json')
// 处理数据,直接在读取文件之后,使用map进行处理即可
.map(fp.toUpper)
.flatMap(print)
.join()