[译]Reduce(软件编写)(第五部分)
- 原文地址:Reduce (Composing Software)(part 5)
- 原文作者:Eric Elliott
- 譯文出自:掘金翻譯計劃
- 譯者:yoyoyohamapi
- 校對者:avocadowang Aladdin-ADD
Smoke Art Cubes to Smoke?—?MattysFlicks?—?(CC BY 2.0) (譯注:該圖是用 PS 將煙霧處理成方塊狀后得到的效果,參見 flickr。))
注意:這是 “軟件編寫” 系列文章的第五部分,該系列主要闡述如何在 JavaScript ES6+ 中從零開始學習函數式編程和組合化軟件(compositional software)技術(譯注:關于軟件可組合性的概念,參見維基百科 Composability)。后續還有更多精彩內容,敬請期待!
<上一篇 | << 返回第一篇
在函數式編程中,reduce(也稱為:fold,accumulate)允許你在一個序列上迭代,并應用一個函數來處理預先聲明的累積值和當前迭代到的元素。當迭代完成時,將返回這個累積值。許多其他有用的功能都可以通過 reduce 實現。多數時候,reduce 可以說是處理集合(collection)最優雅的方式。
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 的職責在于以某種方式將當前迭代的元素 “合攏(fold)” 到累加值中。reducer 規定了 “合攏” 的手段和方式,完成了對當前元素的 “合攏” 后,reducer 將返回新的累加值,然后, .reduce() 將開始處理數組中的下一個元素。reducer 需要一個初始值才能開始工作,所以絕大多數的 .reduce() 實現都需要接收一個初始值作為參數。
在數組元素求和一例中,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。
在這一例子中,我們傳入了一個匿名函數作為 reducer,但是我們也可以抽象出每次求和的過程為一個具名函數,這使得我們代碼的復用程度更高:
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)復制代碼如你所見,在組合中,順序是非常重要的,如果你調換了 double 和 add1 的順序,你將得到截然不同的結果:
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 規則需要你牢記在心:
現在,我們以 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 難一些,還是函數式編程中重要的工具,這個工具強大在它是一個基礎工具,能夠通過它構建出更多更強大的工具。
下一篇: Functors 與 Categories >
接下來
想學習更多 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、前端、后端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃。
總結
以上是生活随笔為你收集整理的[译]Reduce(软件编写)(第五部分)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Mybatis(三) 映射文件详解
- 下一篇: 研究僧,新生活