函数组合
问题:纯函数和柯里化很容易写出洋葱(一层套一层)代码,形如:h(g(f(x))),实际中遇到的问题如:获取数组的最后一个元素在转换成大写字母,_.toUpper(_.first(_.reverse(array)))
解决:函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
管道
下面这张图标识程序中使用函数处理数据的过程,给fn函数输入参数a,返回结果b,可以想象a通过一个管道得到了b数据。
fn = compose(f1, f2, f3)
b = fn(a)
函数组合的概念
函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
- 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
- 函数组合默认是从右到左执行
函数组合示例:
function compose(f, g) {
// 组合完成后返回一个函数,此函数接收一个参数
return function(value) {
// 依次从右向左执行函数
return f(g(value))
}
}
// 使用:获取数组中的最后一个元素(使用反转+取第一个元素)
function reverse(array) {
return array.reverse()
}
function first(array) {
return array[0]
}
// 使用compose
const last = compose(first, reverse)
console.log(last([1, 2, 3, 4, 5 ,6]))
Lodash中的组合函数
- lodash中的组合函数
- lodash中的组合函数flow()或者flowRight(),他们都可以组合多个函数
- flow()是从左到右运行
- flowRight()是从右到左运行,使用的更多一些
lodash示例:
const _ = require('lodash')
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const f = _.flowRight(toUpper, first, reverse)
console.log(f['one', 'tow', 'three'])
组合函数的实现原理
分析:调用组合函数会一次执行传入的函数,每次执行完成一个函数后会将结果交给下一个要执行的函数
// 传入参数个数不确定,所以使用ES6中展开剩余参数(...args)
function compose(...args) {
// 执行后返回一个函数
return function (value) { // 需要接收一个参数
// 执行完成后需要返回结果
// 实现传入函数的从右往左执行,使用reverse将args反转
// 接下来需要执行args中的函数并将结果给后续使用,reduce正好满足需求
return args.reverse().reduce((acc, fn) => fn(acc), value)
}
}
// ES6简化
const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = s => s.toUpperCase()
const f = compose(toUpper, first, reverse)
console.log(f(['one', 'tow', 'three']))
函数组合-结合律
函数的组合要满足结合律(associativity)
- 我们既可以把g和h组合,还可以把f和g组合,结果都是一样的
示例:
// 结合律
let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h))
// => true
函数组合-调试
如何调试组合函数?
- 首先组合的函数都是纯函数,之前我们说过纯函数有点重有一条是便于调试,那我们便可以使用这一个特点
示例:按照指定格式转换字符串
// 输入字符 'ABC DE FGHI'
// 输出字符 'abc-de-fghi'
// 分析:1.首先需要将字符按照空格分隔:split
// 分析:2.将字符转成小写:toLower
// 分析:3.用-分隔字符:join
const _ = require('lodash')
// split(str, sep) 使用柯里化将其转化为一元参数
const split = _.curry((sep, str) => _.split(str, sep))
// toLower(str) 单参数纯函数,不需要处理,直接使用 _.toLower 即可
// join(array, sep)
const join = _.curry((sep, array) => _.join(array, sep))
// 当我们使用split分割以后得到一个数组,此时不能直接使用toLower
// 此时我们遍历数组再使用toLower处理,需要一个map函数
const map = _.curry((fn, array) => _.map(array, fn))
// 进行函数组合
const f = _.flowRight(join('-'), map(_.toLower), split(' '))
// 以上是我们为调试做的准备,那么如果我们想在执行组合中某个函数后得到结果该怎么办
// 首先我们函数组合中每执行完一个函数都会将结果返回
// 那我们根据这个特点是不是可以在该函数后插入一个log函数呢?
const log = v => {
console.log(v)
// 需要将结果返回供之后函数使用
return v
}
const f = _.flowRight(join('-'), log, map(_.toLower), log, split(' '))
// 此时log可输出,但是我们无法清除区分哪个输出对应哪个log,改造下
const trace = _.curry((tag, v) => {
console.log(tag, v)
return v
})
const f = _.flowRight(join('-'), trace('map之后'), map(_.toLower), trace('split之后'), split(' '))
console.log(f('ABC DE FGHI'))
Lodash中的FP(Function Programming)模块
lodash/fp
- lodash的fp模块提供了实用的对函数式编程友好的方法
- 提供了不可变**auto-curried(已被柯里化) iterates-first(函数优先) data-last(数据滞后)**的方法
示例:lodash中的普通模块与fp模块
// 普通模块
// 未被柯里化的函数:数据优先,函数滞后
const _ = require('lodash')
_.map(['a', 'b', 'c'], _.toUpper)
// => ['A', 'B', 'C']
_.map(['a', 'b', 'c'])
// => ['a', 'b', 'c']
_.split('Hello World', ' ')
// => ['Hello', 'World']
// lodash/fp模块
// 柯里化的函数:函数优先,数据滞后
cnost 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')
示例:使用fp模块重写调试中的例子
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))
console.log(f('ABC DE FGHI'))
Point Free
Point Free:我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的哪个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数
- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
解释示例:组合函数
// 1.没有指明处理的数据
// 2.通过组合函数组合多个运算函数
// 3.定义了join、map、toLower、split运算函数
const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' '))
示例:字符串转换
// 将空格替换为_,大写转换成小写
// Hello World => hello_world
const fp = require('lodash/fp')
// 分析:手写大写转小写,然后正则匹配_替换空格
const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
console.log(f('Hello World'))
通过以上示例我们可以看出Point Free其实就是函数组合
Point Free案例
把一个字符串中的首字母提取并转换成大写,使用. 作为分隔符得到新的字符串
// 输入: world wide web
// 输出:W. W. W
// 分析:1.将字符串按照空格分割
// 分析:2.遍历数组将字母全部转换为大写
// 分析:3.遍历数组取转换后单次的首字母
// 分析:4.使用. 分割获取新字符串
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
console.log(f('world wide web'))
// 通过以上方式我们实现了要求
// 但是我们发现组合中使用了两次map遍历,对性能有一定影像,来优化一下
// 两次map遍历都是对同一个数组操作,所以可以合并一下
const f = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))