- 原文地址:Reduce (Composing Software)(part 5)
- 原文做者:Eric Elliott
- 译文出自:掘金翻译计划
- 译者:yoyoyohamapi
- 校对者:avocadowang Aladdin-ADD
Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0) (译注:该图是用 PS 将烟雾处理成方块状后获得的效果,参见 flickr。))javascript
注意:这是 “软件编写” 系列文章的第五部分,该系列主要阐述如何在 JavaScript ES6+ 中从零开始学习函数式编程和组合化软件(compositional software)技术(译注:关于软件可组合性的概念,参见维基百科 Composability)。后续还有更多精彩内容,敬请期待!前端
在函数式编程中,reduce(也称为:fold,accumulate)容许你在一个序列上迭代,并应用一个函数来处理预先声明的累积值和当前迭代到的元素。当迭代完成时,将返回这个累积值。许多其余有用的功能均可以经过 reduce 实现。多数时候,reduce 能够说是处理集合(collection)最优雅的方式。react
reduce 接受一个 reducer 函数以及一个初始值,最终返回一个累积值。对于 Array.prototype.reduce()
来讲, 初始列表将由 this
指明, 因此列表自己不会做为该函数的参数:android
array.reduce(
reducer: (accumulator: Any, current: Any) => Any,
initialValue: Any
) => accumulator: Any复制代码
咱们利用以下方式对一个数组进行求和:ios
[2, 4, 6].reduce((acc, n) => acc + n, 0); // 12复制代码
对于数组的每步迭代,reducer 函数都会被调用,而且向其传入了累积值和当前迭代到的数组元素。reducer 的职责在于以某种方式将当前迭代的元素 “合拢(fold)” 到累加值中。reducer 规定了 “合拢” 的手段和方式,完成了对当前元素的 “合拢” 后,reducer 将返回新的累加值,而后, .reduce()
将开始处理数组中的下一个元素。reducer 须要一个初始值才能开始工做,因此绝大多数的 .reduce()
实现都须要接收一个初始值做为参数。git
在数组元素求和一例中,reducer 函数第一次调用时,acc
将会以 0
值(该值是传入 .reduce()
方法的第二个参数)开始。而后,reducer 返回了 0
+ 2
(2
是数组的第一个元素), 也就是返回了 2
做为新的累积值。下一步,acc = 2, n = 4
传入了 reducer,reducer返回了 2 + 4
(6
)。在最后一步迭代中,acc = 6, n = 6
, reducer 返回了 12
。迭代完成,.reduce()
返回了最终的累积值 12
。github
在这一例子中,咱们传入了一个匿名函数做为 reducer,可是咱们也能够抽象出每次求和的过程为一个具名函数,这使得咱们代码的复用程度更高:web
const summingReducer = (acc, n) => acc + n;
[2, 4, 6].reduce(summingReducer, 0); // 12复制代码
一般,reduce
的工做过程为由左向右。在 JavaScript 中,咱们也有一个 [].reduceRight()
(译注:MDN -- Array.prototype.reduceRight())方法来让 reduce 由右向左地工做。 具体说来,若是你对数组 [2, 4, 6]
应用 .reduceRight()
,第一个被迭代到的元素就将是 6
,最后一个迭代到的元素就是 2
。编程
别吃惊,reduce 确实无所不能,你所熟悉的 map()
,filter()
,forEach()
以及其余函数均可借助于 reduce 来建立。
Map:
const map = (fn, arr) => arr.reduce((acc, item, index, arr) => {
return acc.concat(fn(item, index, arr));
}, []);复制代码
对于 map 来讲,咱们的累积值就是一个新的数组对象,该数组对象中的每一个元素都由原数组对应元素映射获得。累积数组中新的元素由传入 map 的映射函数(fn
)所肯定:对于当前迭代到的元素 item
,咱们经过 fn
计算出新的元素,并将其拼接入累加数组 acc
中。
Filter:
const filter = (fn, arr) => arr.reduce((newArr, item) => {
return fn(item) ? newArr.concat([item]) : newArr;
}, []);复制代码
filter 的工做方式与 map 相似,只不过原数组的元素只有经过一个真值检测函数(predicate function)才能被送入新的累积数组中。亦即,相较于 map,filter 是有条件地选择元素到累积数组中,而且不会改变元素的值。
上面几个例子,你处理的数据都是一些数值序列,你在数值序列上应用指定的函数迭代数据,并将结果合拢到累积值中。大多数应用都所以开始雏形初备,可是你想过这个问题:假如你的序列是函数序列呢?
Compose:
reduce 也是实现函数组合的便捷渠道。假如你想用将函数 g
的输出做为函数 f
的输入,即组合这两个函数: f . g
,那么你可使用下面的 JavaScript 代码片,它没有任何的抽象:
f(g(x))复制代码
reduce 让咱们能抽象出函数组合过程,从而让你也能轻易地实现更多层次的函数组合:
f(g(h(x)))复制代码
为了使函数组合是由右向左的,咱们就要使用上面提到的 .reduceRight()
方法来抽象函数组合过程:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);复制代码
注意:若是 JavaScript 的版本没有提供
[].reduceRight()
,你能够借助于reduce
实现该方法。该实现留给读者本身思考。
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)复制代码
发现问题没有?第一步(加1操做)是 compose 序列上的最后一个元素,因此,compose
须要你自底向上地分析流程的执行。
咱们使用 reduce 由左向右的经常使用特性取代由右向左的组合方式,以示区别,咱们用 pipe
来描述新的组合方式:
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);复制代码
如今,新的流程就能够这么撰写:
const add1ThenDouble = pipe(
add1,
double
);
add1ThenDouble(2); // 6
// ((2 + 1 = 3) * 2 = 6)复制代码
如你所见,在组合中,顺序是很是重要的,若是你调换了 double
和 add1
的顺序,你将获得大相径庭的结果:
const doubleThenAdd1 = pipe(
double,
add1
);
doubleThenAdd1(2); // 5复制代码
以后,咱们还会讨论跟多的关于 compose()
和 pipe()
的细节。如今,你所要知道的只是,reduce()
是一个极为强大的工具,所以必定要掌握它。 若是在学习过程当中遇到了挫折,也大可没必要灰心,不少开发者都花了大量时间才能掌握 reduce。
你可能据说过 “reducer” 这个术语被用于描述 Redux 的状态更新。这篇文章撰写之时,对于使用了 React 或者 Angular 进行构建的 web 应用来讲,Redux 是最流行的状态管理库/架构(Angualar 中的类 Redux 管理是 ngrx/store )。
Redux 使用了 reducer 函数来管理应用状态。一个 Redux 风格的 reducer 接收一个当前应用状态 state
和 和交互对象 action
做为参数(译注:当前状态就至关于累积值,而 action 就至关于目前处理的元素),处理完成后,返回一个新的应用状态:
reducer(state: Any, action: { type: String, payload: Any}) => newState: Any复制代码
Redux 的一些 reducer 规则须要你牢记在心:
如今,咱们以 Redux 风格重写上面的求和 reducer,该 reducer 的行为将由 action 类型决定:
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 (译注:即接收 accumulator
和 current
两个参数的 reducer ),这意味着你将 Redux 中的 reducer 插入到任何现有的 reduce()
实现中去,好比最经常使用的 [].reduce()
。以此为例,咱们能够建立一个 action 对象的数组,并对其进行 reduce 操做,传入 reduce()
的将是咱们定义好的 summingReducer
,据此,咱们得到一个状态快照。以后,一旦对 Redux 中的状态树(store)分派了一样的 action 序列,那么必定能俘获到相同的状态快照:
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 的强大甚至是无所不能了。虽然,理解 reduce 要比理解 map 或者 filter 难一些,仍是函数式编程中重要的工具,这个工具强大在它是一个基础工具,可以经过它构建出更多更强大的工具。
想学习更多 JavaScript 函数式编程吗?
跟着 Eric Elliott 学 Javacript,机不可失时再也不来!
Eric Elliott 是 “编写 JavaScript 应用” (O’Reilly) 以及 “跟着 Eric Elliott 学 Javascript” 两书的做者。他为许多公司和组织做过贡献,例如 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN 和 BBC 等 , 也是不少机构的顶级艺术家,包括但不限于 Usher、Frank Ocean 以及 Metallica。
大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一块儿。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、React、前端、后端、产品、设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划。