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
應用到 x
的 g
的結果上,即組合 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 規則:
- 不帶參數的 reducer 調用應該返回其有效的初始狀態。
- 如果 reducer 不打算處理 action 類型,它依然需要返回狀態。
- 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 更難理解點,不過它是函數式編程實用程序包中必不可少的一個工具 — 一個你可以用來做出很多其它好用工具的工具。