组合软件: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 应用到 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 更难理解点,不过它是函数式编程实用程序包中必不可少的一个工具 — 一个你可以用来做出很多其它好用工具的工具。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容