組合軟件: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 更難理解點,不過它是函數式編程實用程序包中必不可少的一個工具 — 一個你可以用來做出很多其它好用工具的工具。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章