組合軟件:5. Reduce

https://www.zcfy.cc/article/reduce-composing-software-javascript-scene-medium-2697.html

組合軟件:5. Reduce

原文鏈接: medium.com

Reduce(亦稱:fold、accumulate,譯為歸納)實用程序通常用于函數(shù)式編程中,讓我們可以遍歷一個列表,將一個函數(shù)應用到一個累加的值以及列表中的下一個條目,直到迭代完成,并且返回累加值。用 reduce 可以實現(xiàn)很多有用的東西。如果要在一個條目集合上執(zhí)行一些重要的處理,那么 reduce 就是最優(yōu)雅的方式。

Reduce 以一個 reducer 函數(shù)和一個初始值為參數(shù),并返回一個累加值。對于 Array.prototype.reduce(),初始列表是由 this 提供的,所以它并非實參之一:

array.reduce(
  reducer: (accumulator: Any, current: Any) => Any,
  initialValue: Any
) => accumulator: Any

下面我們來對一個數(shù)組求和:

[2, 4, 6].reduce((acc, n) => acc + n, 0); // 12

對于數(shù)組中的每個元素,reducer 被調(diào)用,并將累加器和當前值作為參數(shù)傳入。在某種程度上,reducer 的工作就是將當前值歸納成累加值。代碼中并沒有指定如何歸納,而這正是 reducer 函數(shù)的用途。reducer 返回新的累加值,然后 reduce() 移到數(shù)組中的下一個值。reducer 得要一個初始值開頭,所以大多數(shù)實現(xiàn)會帶一個初始值為形參。

在上面這個求和的 reducer 例子中,當 reducer 第一次被調(diào)用時,acc 是從 0開始(即我們傳遞給 .reduce() 作為第二個參數(shù)的值)。reducer 返回 0 + 2(2 是數(shù)組中第一個元素),即 2。下一次調(diào)用時,acc = 2, n = 4,reducer 返回的結(jié)果為 2 + 4(即 6)。在最后一次迭代中,acc = 6, n = 6,reducer 返回 12。既然迭代完成了,.reduce() 就返回最終的累加值,12。

在本例中,我們將一個匿名 reduce 函數(shù)傳進來做為參數(shù),不過我們可以把它抽象出來,并給它一個名字:

const summingReducer = (acc, n) => acc + n;
[2, 4, 6].reduce(summingReducer, 0); // 12

通常,reduce() 是從左向右執(zhí)行。在 JavaScript 中,我們還有一個 [].reduceRight(),它是從右向左執(zhí)行。也就是說,如果將 .reduceRight() 應用到 [2, 4, 6],那么第一次迭代就是用 6 作為 n 的第一個值,并且向后執(zhí)行,以 2 結(jié)束。

萬能的 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 來說,我們的累加值是一個新數(shù)組,新數(shù)組中的每一個新元素對應于原始數(shù)組中的每個值。新元素的值是對 arr 實參中每個元素應用傳遞進來的映射函數(shù)(fn)后生成的。通過對當前元素調(diào)用 fn,我們將新數(shù)組累加起來,并把結(jié)果連接給累加器數(shù)組 acc。

Filter:

const filter = (fn, arr) => arr.reduce((newArr, item) => {
  return fn(item) ? newArr.concat([item]) : newArr;
}, []);

Filter 與 map 的工作方式大致相同,不同之處在于我們是以一個斷言函數(shù)為參數(shù),如果元素通過了斷言測試(即 fn(item) 返回 true),就有條件地將當前值添加到新數(shù)組中。

對于上面的每個示例,我們都有一個數(shù)據(jù)列表,遍歷該數(shù)據(jù),同時對該數(shù)據(jù)應用一些函數(shù),并將結(jié)果合攏為一個累加值。應該很多應用程序可以浮現(xiàn)在腦海中。不過,如果你的數(shù)據(jù)是一個函數(shù)的列表該怎么辦呢?

Compose:

Reduce 還是一種最方便的組合函數(shù)的方式。還記得函數(shù)組合吧:如果想把函數(shù) f 應用到 xg 的結(jié)果上,即組合 f . g,可以用如下的 JavaScript 來表示:

f(g(x))

Reduce 讓我們可以把這個過程抽象出來,讓它可以用于任意數(shù)量的函數(shù)上,這樣我們就很容易定義一個函數(shù)來表示如下組合:

f(g(h(x)))

要做到這點,我們需要反著執(zhí)行 reduce。即,從右到左,而不是從左到右。謝天謝地,JavaScript 提供了一個 .reduceRight() 方法:

const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

注意:就算 JavaScript 沒有提供 [].reduceRight(),我們依然可以使用 reduce() 實現(xiàn) reduceRight()。我把這個難題留給喜歡冒險的讀者去搞定。

Pipe:

如果我們想從內(nèi)到外(即按數(shù)學符號的意義)表示組合,那么 compose() 就挺好。但是如果我們想把它當作是一連串的事件又該怎么辦呢?

假設(shè)我們想給一個數(shù)加 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);

現(xiàn)在你可以像如下這樣寫 add1ThenDouble()

const add1ThenDouble = pipe(
  add1,
  double
);

add1ThenDouble(2); // 6
// ((2 + 1 = 3) * 2 = 6)

這是很重要的,因為如果向后組合的話,有時會得到不同的結(jié)果:

const doubleThenAdd1 = pipe(
  double,
  add1
);

doubleThenAdd1(2); // 5

之后我們會更深入研究 compose()pipe()。現(xiàn)在你應該理解的是,reduce() 是一個很強大的工具,并且你確實需要學它。只是要注意的是,如果你用 reduce 太復雜的話,有些人可能會很難看懂。

談談 Redux

你可能聽說過術(shù)語 "reducer" 用來描述 Redux 的重要狀態(tài)更新。在撰寫本文時,Redux 是用 React 和 Angular(后者是通過 ngrx/store)創(chuàng)建 Web 應用程序的最熱門的狀態(tài)管理庫和框架。

Redux 用 reducer 函數(shù)管理應用程序狀態(tài)。Redux 風格的 reducer 以當前狀態(tài)和一個 action 對象為參數(shù),并返回一個新狀態(tài):

reducer(state: Any, action: { type: String, payload: Any}) => newState: Any

Redux 中有一些需要記住的 reducer 規(guī)則:

  1. 不帶參數(shù)的 reducer 調(diào)用應該返回其有效的初始狀態(tài)。
  2. 如果 reducer 不打算處理 action 類型,它依然需要返回狀態(tài)。
  3. Redux 的 reducer 必須是純函數(shù)。

下面我們將求和 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 函數(shù)簽名的 reduce() 實現(xiàn)中的標準 reducer,包括 [].reduce()。就是說,我們可以先創(chuàng)建一個 action 對象數(shù)組,如果這些相同的行為被分發(fā)到 store 中,我們就對它們 reduce,從而得到一個狀態(tài)快照來代表該有的同一狀態(tài):

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 做單元測試變得易如反掌。

總結(jié)

你應該開始看到 reduce 是極為有用并且通用的抽象。它肯定比 map 或者 filter 更難理解點,不過它是函數(shù)式編程實用程序包中必不可少的一個工具 —?一個你可以用來做出很多其它好用工具的工具。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容