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 應用到 x 的 g 的結(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ī)則:
- 不帶參數(shù)的 reducer 調(diào)用應該返回其有效的初始狀態(tài)。
- 如果 reducer 不打算處理 action 類型,它依然需要返回狀態(tài)。
- 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ù)式編程實用程序包中必不可少的一個工具 —?一個你可以用來做出很多其它好用工具的工具。