函数式编程 – 组合compose

37次阅读

共计 3447 个字符,预计需要花费 9 分钟才能阅读完成。

函数式编程中有一个比较重要的概念就是函数组合(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 就可以利用 reduceRight
compose = (…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)

最后
无论是 compose,还是后面提到的 pipe,概念非常简单,都可以使用非常巧妙的方式实现(大部分使用 reduce),而且在编程中很大程度上简化代码。最后列出优秀框架中使用 compose 的示例:

redux/compose
koa-Compose
underscorejs/compose

参考链接:

Creating an ES6ish Compose in Javascript
compose.js
Optimization-killers

正文完
 0