[譯]Reduce(軟件編寫)(第五部分)

Smoke Art Cubes to Smoke — MattysFlicks — (CC BY 2.0) (譯註:該圖是用 PS 將煙霧處理成方塊狀後獲得的效果,參見 flickr。))javascript

注意:這是 「軟件編寫」 系列文章的第五部分,該系列主要闡述如何在 JavaScript ES6+ 中從零開始學習函數式編程和組合化軟件(compositional software)技術(譯註:關於軟件可組合性的概念,參見維基百科 Composability)。後續還有更多精彩內容,敬請期待!前端

<上一篇 | << 返回第一篇java

在函數式編程中,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 + 22 是數組的第一個元素), 也就是返回了 2 做爲新的累積值。下一步,acc = 2, n = 4 傳入了 reducer,reducer返回了 2 + 46)。在最後一步迭代中,acc = 6, n = 6, reducer 返回了 12。迭代完成,.reduce() 返回了最終的累積值 12github

在這一例子中,咱們傳入了一個匿名函數做爲 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

別吃驚,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)複製代碼

如你所見,在組合中,順序是很是重要的,若是你調換了 doubleadd1 的順序,你將獲得大相徑庭的結果:

const doubleThenAdd1 = pipe(
  double,
  add1
);

doubleThenAdd1(2); // 5複製代碼

以後,咱們還會討論跟多的關於 compose()pipe() 的細節。如今,你所要知道的只是,reduce() 是一個極爲強大的工具,所以必定要掌握它。 若是在學習過程當中遇到了挫折,也大可沒必要灰心,不少開發者都花了大量時間才能掌握 reduce。

Redux 中的 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 規則須要你牢記在心:

  1. 一個 reducer 若是進行了無參調用,它要返回它的初始狀態。
  2. 若是 reducer 操縱的 action 沒有聲明類型,他要返回當前狀態。
  3. 最最重要的是,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 (譯註:即接收 accumulatorcurrent 兩個參數的 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 難一些,仍是函數式編程中重要的工具,這個工具強大在它是一個基礎工具,可以經過它構建出更多更強大的工具。

下一篇: Functors 與 Categories >

接下來

想學習更多 JavaScript 函數式編程嗎?

跟着 Eric Elliott 學 Javacript,機不可失時再也不來!

Eric Elliott「編寫 JavaScript 應用」 (O’Reilly) 以及 「跟着 Eric Elliott 學 Javascript」 兩書的做者。他爲許多公司和組織做過貢獻,例如 Adobe SystemsZumba FitnessThe Wall Street JournalESPNBBC 等 , 也是不少機構的頂級藝術家,包括但不限於 UsherFrank Ocean 以及 Metallica

大多數時間,他都在 San Francisco Bay Area,同這世上最美麗的女子在一塊兒。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃

相關文章
相關標籤/搜索