函数式编程中有一个比较重要的概念就是函数组合(compose),组合多个函数,同时返回一个新的函数。调用时,组合函数按顺序从右向左执行。右边函数调用后,返回的结果,作为左边函数的参数传入,严格保证了执行顺序,这也是compose 主要特点。
入门简介
组合两个函数
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,也让我们了解了组合的特点,接着我们看看如何组合更多的函数,因为在实际应用中,不会像入门介绍的代码那么简单。
主要注意几个关键点:
- 利用arguments的长度得到所有组合函数的个数
- 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
初始值。
实现compose
pipe 部分,利用reduce实现,反过来看,
compose
就可以利用reduceRightcompose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
利用递归
compose = (fn, ...fns) => fns.length === 0 ? fn: (...args) => fn(compose(...fns)(...args))
递归代码,首先看出口条件,
fns.length === 0
, 最后一定执行最左边的函数,然后把剩下的函数再经过compose
调用,利用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)
- 入口 composeFunc(arr), 第一次迭代,reduce函数执行 (getFirst, trace) => (…args)=>getFirst(trace(…args)),函数
最后
无论是compose, 还是后面提到的pipe,概念非常简单,都可以使用非常巧妙的方式实现(大部分使用reduce),而且在编程中很大程度上简化代码。最后列出优秀框架中使用compose的示例:
参考链接: