- 原文地址:Reduce (Composing Software)(part 5)
- 原文做者:Eric Elliott
- 譯文出自:掘金翻譯計劃
- 譯者:yoyoyohamapi
- 校對者:avocadowang Aladdin-ADD
Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0) (譯註:該圖是用 PS 將煙霧處理成方塊狀後獲得的效果,參見 flickr。))javascript
注意:這是 「軟件編寫」 系列文章的第五部分,該系列主要闡述如何在 JavaScript ES6+ 中從零開始學習函數式編程和組合化軟件(compositional software)技術(譯註:關於軟件可組合性的概念,參見維基百科 Composability)。後續還有更多精彩內容,敬請期待!前端
在函數式編程中,reduce(也稱爲:fold,accumulate)容許你在一個序列上迭代,並應用一個函數來處理預先聲明的累積值和當前迭代到的元素。當迭代完成時,將返回這個累積值。許多其餘有用的功能均可以經過 reduce 實現。多數時候,reduce 能夠說是處理集合(collection)最優雅的方式。react
reduce 接受一個 reducer 函數以及一個初始值,最終返回一個累積值。對於 Array.prototype.reduce()
來講, 初始列表將由 this
指明, 因此列表自己不會做爲該函數的參數:android
array.reduce(
reducer: (accumulator: Any, current: Any) => Any,
initialValue: Any
) => accumulator: Any複製代碼
咱們利用以下方式對一個數組進行求和:ios
[2, 4, 6].reduce((acc, n) => acc + n, 0); // 12複製代碼
對於數組的每步迭代,reducer 函數都會被調用,而且向其傳入了累積值和當前迭代到的數組元素。reducer 的職責在於以某種方式將當前迭代的元素 「合攏(fold)」 到累加值中。reducer 規定了 「合攏」 的手段和方式,完成了對當前元素的 「合攏」 後,reducer 將返回新的累加值,而後, .reduce()
將開始處理數組中的下一個元素。reducer 須要一個初始值才能開始工做,因此絕大多數的 .reduce()
實現都須要接收一個初始值做爲參數。git
在數組元素求和一例中,reducer 函數第一次調用時,acc
將會以 0
值(該值是傳入 .reduce()
方法的第二個參數)開始。而後,reducer 返回了 0
+ 2
(2
是數組的第一個元素), 也就是返回了 2
做爲新的累積值。下一步,acc = 2, n = 4
傳入了 reducer,reducer返回了 2 + 4
(6
)。在最後一步迭代中,acc = 6, n = 6
, reducer 返回了 12
。迭代完成,.reduce()
返回了最終的累積值 12
。github
在這一例子中,咱們傳入了一個匿名函數做爲 reducer,可是咱們也能夠抽象出每次求和的過程爲一個具名函數,這使得咱們代碼的複用程度更高:web
const summingReducer = (acc, n) => acc + n;
[2, 4, 6].reduce(summingReducer, 0); // 12複製代碼
一般,reduce
的工做過程爲由左向右。在 JavaScript 中,咱們也有一個 [].reduceRight()
(譯註:MDN -- Array.prototype.reduceRight())方法來讓 reduce 由右向左地工做。 具體說來,若是你對數組 [2, 4, 6]
應用 .reduceRight()
,第一個被迭代到的元素就將是 6
,最後一個迭代到的元素就是 2
。編程
別吃驚,reduce 確實無所不能,你所熟悉的 map()
,filter()
,forEach()
以及其餘函數均可藉助於 reduce 來建立。
Map:
const map = (fn, arr) => arr.reduce((acc, item, index, arr) => {
return acc.concat(fn(item, index, arr));
}, []);複製代碼
對於 map 來講,咱們的累積值就是一個新的數組對象,該數組對象中的每一個元素都由原數組對應元素映射獲得。累積數組中新的元素由傳入 map 的映射函數(fn
)所肯定:對於當前迭代到的元素 item
,咱們經過 fn
計算出新的元素,並將其拼接入累加數組 acc
中。
Filter:
const filter = (fn, arr) => arr.reduce((newArr, item) => {
return fn(item) ? newArr.concat([item]) : newArr;
}, []);複製代碼
filter 的工做方式與 map 相似,只不過原數組的元素只有經過一個真值檢測函數(predicate function)才能被送入新的累積數組中。亦即,相較於 map,filter 是有條件地選擇元素到累積數組中,而且不會改變元素的值。
上面幾個例子,你處理的數據都是一些數值序列,你在數值序列上應用指定的函數迭代數據,並將結果合攏到累積值中。大多數應用都所以開始雛形初備,可是你想過這個問題:假如你的序列是函數序列呢?
Compose:
reduce 也是實現函數組合的便捷渠道。假如你想用將函數 g
的輸出做爲函數 f
的輸入,即組合這兩個函數: f . g
,那麼你可使用下面的 JavaScript 代碼片,它沒有任何的抽象:
f(g(x))複製代碼
reduce 讓咱們能抽象出函數組合過程,從而讓你也能輕易地實現更多層次的函數組合:
f(g(h(x)))複製代碼
爲了使函數組合是由右向左的,咱們就要使用上面提到的 .reduceRight()
方法來抽象函數組合過程:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);複製代碼
注意:若是 JavaScript 的版本沒有提供
[].reduceRight()
,你能夠藉助於reduce
實現該方法。該實現留給讀者本身思考。
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)複製代碼
發現問題沒有?第一步(加1操做)是 compose 序列上的最後一個元素,因此,compose
須要你自底向上地分析流程的執行。
咱們使用 reduce 由左向右的經常使用特性取代由右向左的組合方式,以示區別,咱們用 pipe
來描述新的組合方式:
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);複製代碼
如今,新的流程就能夠這麼撰寫:
const add1ThenDouble = pipe(
add1,
double
);
add1ThenDouble(2); // 6
// ((2 + 1 = 3) * 2 = 6)複製代碼
如你所見,在組合中,順序是很是重要的,若是你調換了 double
和 add1
的順序,你將獲得大相徑庭的結果:
const doubleThenAdd1 = pipe(
double,
add1
);
doubleThenAdd1(2); // 5複製代碼
以後,咱們還會討論跟多的關於 compose()
和 pipe()
的細節。如今,你所要知道的只是,reduce()
是一個極爲強大的工具,所以必定要掌握它。 若是在學習過程當中遇到了挫折,也大可沒必要灰心,不少開發者都花了大量時間才能掌握 reduce。
你可能據說過 「reducer」 這個術語被用於描述 Redux 的狀態更新。這篇文章撰寫之時,對於使用了 React 或者 Angular 進行構建的 web 應用來講,Redux 是最流行的狀態管理庫/架構(Angualar 中的類 Redux 管理是 ngrx/store )。
Redux 使用了 reducer 函數來管理應用狀態。一個 Redux 風格的 reducer 接收一個當前應用狀態 state
和 和交互對象 action
做爲參數(譯註:當前狀態就至關於累積值,而 action 就至關於目前處理的元素),處理完成後,返回一個新的應用狀態:
reducer(state: Any, action: { type: String, payload: Any}) => newState: Any複製代碼
Redux 的一些 reducer 規則須要你牢記在心:
如今,咱們以 Redux 風格重寫上面的求和 reducer,該 reducer 的行爲將由 action 類型決定:
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 (譯註:即接收 accumulator
和 current
兩個參數的 reducer ),這意味着你將 Redux 中的 reducer 插入到任何現有的 reduce()
實現中去,好比最經常使用的 [].reduce()
。以此爲例,咱們能夠建立一個 action 對象的數組,並對其進行 reduce 操做,傳入 reduce()
的將是咱們定義好的 summingReducer
,據此,咱們得到一個狀態快照。以後,一旦對 Redux 中的狀態樹(store)分派了一樣的 action 序列,那麼必定能俘獲到相同的狀態快照:
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 的強大甚至是無所不能了。雖然,理解 reduce 要比理解 map 或者 filter 難一些,仍是函數式編程中重要的工具,這個工具強大在它是一個基礎工具,可以經過它構建出更多更強大的工具。
想學習更多 JavaScript 函數式編程嗎?
跟着 Eric Elliott 學 Javacript,機不可失時再也不來!
Eric Elliott 是 「編寫 JavaScript 應用」 (O’Reilly) 以及 「跟着 Eric Elliott 學 Javascript」 兩書的做者。他爲許多公司和組織做過貢獻,例如 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN 和 BBC 等 , 也是不少機構的頂級藝術家,包括但不限於 Usher、Frank Ocean 以及 Metallica。
大多數時間,他都在 San Francisco Bay Area,同這世上最美麗的女子在一塊兒。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。