函数式编程中有一个比较重要的概念就是函数组合(compose),组合多个函数,同时返回一个新的函数。调用时,组合函数按顺序从右向左执行。右边函数调用后,返回的结果,作为左边函数的参数传入,严格保证了执行顺序,这也是compose 主要特色。| 函数式编程--函数组合(Function composition) - 知乎
入门简介
组合两个函数
compose 非常简单,通过下面的示例代码,就非常清楚。
function compose (f, g) {
return function(x) {
return f(g(x));
}
}
var arr = [1, 2, 3],
reverse = function(x){ return x.reverse()},
getFirst = function(x) {return x[0]},
compseFunc = compose(getFirst, reverse);
compseFunc(arr); // 3
参数在函数间就好像经过‘管道’传输同样,最右边的函数接收外界参数,返回结果传给左边的函数,最后输出结果。
组合任意个函数
上面组合了两个函数的compose,也让咱们了解了组合的特点,接着咱们看看如何组合更多的函数,由于在实际应用中,不会像入门介绍的代码那么简单。
主要注意几个关键点:
1、利用arguments的长度得到所有组合函数的个数
2、reduce 遍历执行全部函数。
var compose = function() {
var args = Array.prototype.slice.call(arguments);
return function(x) {
if (args.length >= 2) {
return args.reverse().reduce((p, c) => {
return p = c(p)
}, x)
} else {
return args[1] && args[1](x);
}
}
}
// 利用上面示例 测试一下。
var arr = [1, 2, 3],
reverse = function(x){ return x.reverse()},
getFirst = function(x) {return x[0]},
trace = function(x) { console.log('执行结果:', x); return x}
compseFunc = compose(trace, getFirst, trace, reverse);
compseFunc(arr);
// 执行结果: (3) [3, 2, 1]
// 执行结果: 3
// 3
如此实现,基本没什么问题,变量arr
在管道中传入后,经过各种操作,最后返回了结果。
深入理解
认识pipe
函数式编程(FP)里面跟compose类似的方法,就是pipe。
pipe,主要作用也是组合多个函数,称之为“流”, 肯定得按照正常方法,从左往右调用函数,与compose 调用方法相反。
ES6 实现Compose function
先看下compose 最基础的两参数版本
const compose = (f1, f2) => value => f1(f2(value));
利用箭头函数,非常直接的表明两个函数嵌套执行的关系,接着看多层嵌套。
(f1, f2, f3...) => value => f1(f2(f3));
抽象出来表示:
() => () => result;
先提出这些基础的组合方式,对我们后面理解高级ES6方法实现compose有很大帮助。
实现pipe
前面提到 pipe 是反向的compose,pipe正向调用也致使它实现起来更容易。
pipe = (...fns) => x => fns.reduce((v, f) => f(v), x)
一行代码就实现了pipe,套用上面抽象出来的表达式,reduce
恰好正向遍历全部函数, 参数x
做为传递给函数的初始值, 后面每次f(v)
执行的结果,作为下一次f(v)
调用的参数v
,完成了函数组合调用。
或者,能够把函数组合中,第一个函数获取参数后,获得的结果,作为reduce
遍历的初始值。
pipe = (fn,...fns) => (x) => fns.reduce( (v, f) => f(v), fn(x));
利用ES6提供的rest 参数 ,用于获取函数的多余参数,提取出第一个函数fn,多余函数参数放到fns中,fns看成是数组,也不用像arguments那种事先经过Array.prototype.slice.call
转为数组,arguments对性能损耗也可以避免。 fn(x)
第一个函数执行结果做为reduce
初始值。
注:关于剩余参数rest,可参考我之前文章:【JS高级】ES6参数增强之剩余参数的应用
实现compose
1、pipe 部分,利用reduce实现,反过来看,compose
就能够利用reduceRight
compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
2、利用递归
compose = (fn, ...fns) => fns.length === 0 ? fn: (...args) => fn(compose(...fns)(...args))
递归代码,首先看出口条件, fns.length === 0
,最后一定执行最左边的函数,然后把剩下的函数再经过compose
调用,
3、利用reduce实现。
一行实现,并且仍是用正向的 reduce
。
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)))
作者其实用例子作了解释,可以看下reduce
迭代的方向是从左往右的,而compose
要求执行的方向是从右往左。对数组中每一项执行函数,正常状况下都应该放回执行结果,比如(v, f) => f(v)
,返回f(v)
执行结果,这里是(f, g) => (...args) => f(g(...args))
返回一个函数(...args) => f(g(...args))
,这样就能够保证后面的函数g
在被作为参数传入时比前面的函数f
先执行。
简单利用前面的组合两个函数的例子分析一下。
...
composeFunc = compose(getFirst, trace, reverse);
composeFunc(arr);
主要看reduce 函数里面的执行过程:
◆ 入口 composeFunc(arr), 第一次迭代,reduce函数执行 (getFirst, trace) => (...args)=>getFirst(trace(...args)),函数(...args)=>getFirst(trace(...args))
作为下一次迭代中累计器f
的值。
◆ 第二次迭代,reduce函数中
f == (...args)=>getFirst(trace(...args))
g == reverse。
// 替换一下 (f, g) => (...args) => f(g(...args))
((...args)=>getFirst(trace(...args)), reverse) => (...args) => ((...args)=>getFirst(trace(...args)))(reverse(...args))
◆ 迭代结束,最后得到的comoseFunc就是
// 对照第二次的执行结果, (...args) => f(g(...args))
(...args) => ((...args)=>getFirst(trace(...args)))(reverse(...args))
◆ 调用函数composeFunc(arr)。
(arr) => ((...args)=>getFirst(trace(...args)))(reverse(arr))
===》reverse(arr) 执行结果[3, 2, 1] 做为参数
((...args)=>getFirst(trace(...args)))([3,2,1])
==》入参调用函数
getFirst(trace[3,2,1])
===》
getFirst([3, 2, 1])
===》
结果为 3
非常巧妙的把后一个函数的执行结果作为包裹着前面函数的空函数的参数,传入执行。其中大量用到下面的结构。
((arg)=> f(arg))(arg)
// 转换一下。
(function(x) {
return f(x)
})(x)
最后
不管是compose, 仍是后面提到的pipe,概念很是简单,均可以使用很是巧妙的方式实现(大部分使用reduce),并且在编程中很大程度上简化代码。最后列出优秀框架中使用compose的示例:
- redux/compose
- koa-Compose
- underscorejs/compose
参考连接:
- Creating an ES6ish Compose in Javascript
- compose.js
- Optimization-killers
参考资料:
JS高级编程中compose函数的介绍和基本实现 | 复合函数compose函数的概念
compose函数 | 函数式编程--函数组合(Function composition) | 函数式编程之compose