网络埋伏纪事

组合软件:5. Reduce

网络埋伏纪事 · 2017-05-05翻译 · 1103阅读 原文链接

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 应用到 xg 的结果上,即组合 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 规则:

  1. 不带参数的 reducer 调用应该返回其有效的初始状态。
  2. 如果 reducer 不打算处理 action 类型,它依然需要返回状态。
  3. 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 更难理解点,不过它是函数式编程实用程序包中必不可少的一个工具 — 一个你可以用来做出很多其它好用工具的工具。

相关文章