组合软件:5. Reduce
https://www.zcfy.cc/article/reduce-composing-software-javascript-scene-medium-2697.html
组合软件:5. Reduce
原文链接: medium.com
Reduce(亦称:fold、accumulate,译为归纳)实用程序通常用于函数式编程中,让我们可以遍历一个列表,将一个函数应用到一个累加的值以及列表中的下一个条目,直到迭代完成,并且返回累加值。用 reduce 可以实现很多有用的东西。如果要在一个条目集合上执行一些重要的处理,那么 reduce 就是最优雅的方式。
Reduce 以一个 reducer 函数和一个初始值为参数,并返回一个累加值。对于 Array.prototype.reduce()
,初始列表是由 this
提供的,所以它并非实参之一:
array.reduce(
reducer: (accumulator: Any, current: Any) => Any,
initialValue: Any
) => accumulator: Any
下面我们来对一个数组求和:
[2, 4, 6].reduce((acc, n) => acc + n, 0); // 12
对于数组中的每个元素,reducer 被调用,并将累加器和当前值作为参数传入。在某种程度上,reducer 的工作就是将当前值归纳成累加值。代码中并没有指定如何归纳,而这正是 reducer 函数的用途。reducer 返回新的累加值,然后 reduce()
移到数组中的下一个值。reducer 得要一个初始值开头,所以大多数实现会带一个初始值为形参。
在上面这个求和的 reducer 例子中,当 reducer 第一次被调用时,acc
是从 0
开始(即我们传递给 .reduce()
作为第二个参数的值)。reducer 返回 0
+ 2
(2
是数组中第一个元素),即 2
。下一次调用时,acc = 2, n = 4
,reducer 返回的结果为 2 + 4
(即 6
)。在最后一次迭代中,acc = 6, n = 6
,reducer 返回 12
。既然迭代完成了,.reduce()
就返回最终的累加值,12
。
在本例中,我们将一个匿名 reduce 函数传进来做为参数,不过我们可以把它抽象出来,并给它一个名字:
const summingReducer = (acc, n) => acc + n;
[2, 4, 6].reduce(summingReducer, 0); // 12
通常,reduce()
是从左向右执行。在 JavaScript 中,我们还有一个 [].reduceRight()
,它是从右向左执行。也就是说,如果将 .reduceRight()
应用到 [2, 4, 6]
,那么第一次迭代就是用 6
作为 n
的第一个值,并且向后执行,以 2
结束。
万能的 Reduce
Reduce 是个多面手。我们可以很容易用 reduce 来定义 map()
、filter()
、forEach()
以及很多其它有意思的事情:
Map:
const map = (fn, arr) => arr.reduce((acc, item, index, arr) => {
return acc.concat(fn(item, index, arr));
}, []);
对于 map 来说,我们的累加值是一个新数组,新数组中的每一个新元素对应于原始数组中的每个值。新元素的值是对 arr
实参中每个元素应用传递进来的映射函数(fn
)后生成的。通过对当前元素调用 fn
,我们将新数组累加起来,并把结果连接给累加器数组 acc
。
Filter:
const filter = (fn, arr) => arr.reduce((newArr, item) => {
return fn(item) ? newArr.concat([item]) : newArr;
}, []);
Filter 与 map 的工作方式大致相同,不同之处在于我们是以一个断言函数为参数,如果元素通过了断言测试(即 fn(item)
返回 true
),就有条件地将当前值添加到新数组中。
对于上面的每个示例,我们都有一个数据列表,遍历该数据,同时对该数据应用一些函数,并将结果合拢为一个累加值。应该很多应用程序可以浮现在脑海中。不过,如果你的数据是一个函数的列表该怎么办呢?
Compose:
Reduce 还是一种最方便的组合函数的方式。还记得函数组合吧:如果想把函数 f
应用到 x
的 g
的结果上,即组合 f . g
,可以用如下的 JavaScript 来表示:
f(g(x))
Reduce 让我们可以把这个过程抽象出来,让它可以用于任意数量的函数上,这样我们就很容易定义一个函数来表示如下组合:
f(g(h(x)))
要做到这点,我们需要反着执行 reduce。即,从右到左,而不是从左到右。谢天谢地,JavaScript 提供了一个 .reduceRight()
方法:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
注意:就算 JavaScript 没有提供
[].reduceRight()
,我们依然可以使用reduce()
实现reduceRight()
。我把这个难题留给喜欢冒险的读者去搞定。
Pipe:
如果我们想从内到外(即按数学符号的意义)表示组合,那么 compose()
就挺好。但是如果我们想把它当作是一连串的事件又该怎么办呢?
假设我们想给一个数加 1
,然后对它加倍。用 compose()
的话,将是:
const add1 = n => n + 1;
const double = n => n * 2;
const add1ThenDouble = compose(
double,
add1
);
add1ThenDouble(2); // 6
// ((2 + 1 = 3) * 2 = 6)
看出问题没有?第一个步骤列在最后,所以为了理解这个事件顺序,就需要从列表底部开始,向后到顶部。
或者我们可以像往常一样从左向右 reduce,而不是从右向左:
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
现在你可以像如下这样写 add1ThenDouble()
:
const add1ThenDouble = pipe(
add1,
double
);
add1ThenDouble(2); // 6
// ((2 + 1 = 3) * 2 = 6)
这是很重要的,因为如果向后组合的话,有时会得到不同的结果:
const doubleThenAdd1 = pipe(
double,
add1
);
doubleThenAdd1(2); // 5
之后我们会更深入研究 compose()
和 pipe()
。现在你应该理解的是,reduce()
是一个很强大的工具,并且你确实需要学它。只是要注意的是,如果你用 reduce 太复杂的话,有些人可能会很难看懂。
谈谈 Redux
你可能听说过术语 "reducer" 用来描述 Redux 的重要状态更新。在撰写本文时,Redux 是用 React 和 Angular(后者是通过 ngrx/store
)创建 Web 应用程序的最热门的状态管理库和框架。
Redux 用 reducer 函数管理应用程序状态。Redux 风格的 reducer 以当前状态和一个 action 对象为参数,并返回一个新状态:
reducer(state: Any, action: { type: String, payload: Any}) => newState: Any
Redux 中有一些需要记住的 reducer 规则:
- 不带参数的 reducer 调用应该返回其有效的初始状态。
- 如果 reducer 不打算处理 action 类型,它依然需要返回状态。
- Redux 的 reducer 必须是纯函数。
下面我们将求和 reducer 重写为 Redux 风格的 reducer,让它对 action 对象 reduce:
const ADD_VALUE = 'ADD_VALUE';
const summingReducer = (state = 0, action = {}) => {
const { type, payload } = action;
switch (type) {
case ADD_VALUE:
return state + payload.value;
default: return state;
}
};
对于 Redux 来说,最酷的事是 reducer 只是可以插入到任何遵守 reducer 函数签名的 reduce()
实现中的标准 reducer,包括 [].reduce()
。就是说,我们可以先创建一个 action 对象数组,如果这些相同的行为被分发到 store 中,我们就对它们 reduce,从而得到一个状态快照来代表该有的同一状态:
const actions = [
{ type: 'ADD_VALUE', payload: { value: 1 } },
{ type: 'ADD_VALUE', payload: { value: 1 } },
{ type: 'ADD_VALUE', payload: { value: 1 } },
];
actions.reduce(summingReducer, 0); // 3
这就让对 Redux 风格的 reducer 做单元测试变得易如反掌。
总结
你应该开始看到 reduce 是极为有用并且通用的抽象。它肯定比 map 或者 filter 更难理解点,不过它是函数式编程实用程序包中必不可少的一个工具 — 一个你可以用来做出很多其它好用工具的工具。