产生背景及要解决的问题:
我们使用纯函数和柯里化很容易写成洋葱代码 h(g(f(x))),洋葱代码的嵌套问题使得我们的维护更加困难。这与我们选用函数式编程的开发初衷是相违背的,在这种情况下函数组合的概念就应运而生。函数的组合可以让我们把细粒度的函数重新组合生成一个新的函数,其中一个重要的概念就是数据管道。下面这张图表示程序中使用函数处理数据的过程,给fn函数输入参数返回结果b,可以想象a数据通过一个管道fn得到b数据。
当fn比较复杂的时候,我们可以把fn拆解成多个小函数,此时多了中间运算过程产生的m和n。下面这张图可以想象成fn这个管道拆分成了3个子管道f1、f2、f3 。数据a经过管道f3得到结果m ,m再通过f2得到结果n,n通过f1得到最终的结果b 。
当我们把一个大的函数拆解成多个小的函数之后,我们会多出一些中间结果。而我们在函数组合过程中是不需要考虑中间结果的,我们可以使用伪代码描述下这张图:
fn = compose(f1, f2, f3)
b = fn(a)
概念:
如果一个函数需要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并一个函数。在处理的过程中这些中间函数会得到相应的中间结果,这些中间结果我们不需要关注;此时函数就相当于一个数据的管道,我们的数据通过相应的管道得到相应的结果,函数组合就是把这些管道进行链接。让数据穿过多个管道形成最终的结果;函数组合默认情况下是从右到左执行,并且管道函数都是一元函数;函数组合遵循结合律(associativity)。
// 结合律(associativity)
let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h))
// true
总结:
函数组合可以让我们把多个函数组合成一个新的函数,然后在执行的过程中,我们可以把参数输入给第一个函数,当它执行完成以后会返回一个中间结果。并且把这个中间结果交接下一个函数去处理,当最后一个函数执行完毕之后,我们会把最终结果返回。
函数组合(pipe&compose)代替链式调用:
- 链式调用是以数据为主语
data.fn1().fn2().fn3() - 函数组合是以函数为主语
compose(fn1, fn2, fn3)(data)
相比较而言compose的写法更容易复用函数,数据也保持稳定不会被莫名修改
const ops = compose(...)ops(data)
链式调用侧重于 oop 风格,先有对象,在调用对象的方法。是面向对象编程思想的一个实现:
// 定义对象
class Calculate {
constructor (value) {
this.value = value
}
addOne () {
this.value++
return this
}
multiTwo () {
this.value *= 2
return this
}
}
// 进行链式调用
new Calculate(10).addOne().multiTwo()
函数组合(pipe&compose)将对象和操作对象的方法分离开来,更侧重于对函数(逻辑)的操作(组合),复用性更好!reduce(callback[accumulator, currentValue, currentIndex, array], initialValue)
const pipe = () => { // 从左往右执行函数组合
const args = [].slice.apply(arguments)
return function (value) {
return args.reduce((acc, fn) => fn(acc), value)
}
}
const compose = () => { // 从右往左执行函数(右侧函数的输出为左侧函数的输入)
const args = [].slice.apply(arguments)
return function (value) {
return args.reduceRight((acc, fn) => fn(acc), value)
}
}
Lodash 中的组合函数与 Lodash-fp 模块
Lodash 中通过 flow 与 flowRight 对应实现了 pipe 和 compose;Lodash 的 fp 模块提供了不可变的 、实用的、对函数式编程友好的方法(auto-curried iteratee-first data-last自动柯里化,函数在先,数据在后)
// 普通lodash 模块,函数使用方法
const _ = require('lodash')
_.map(['a', 'b', 'c'], _.toUpper) // 可以看到数据在先,函数在后,更符合传统思维
// => ['A', 'B', 'C']
// 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')