談談 JavaScript 那些常見的 Functional Programming 的概念帶來了怎樣的好處
最近前端逐漸吹起 functional programing 的風,多半的原因是因為 React.js 的浪潮,大家為了追求 virtual dom reconciliation 的效能以及能夠快速的把 state 和 props 變成 Component 所導致的,大家於是開始逐漸的要求專案的撰寫 function 追求 pure 不能有其他的副作用,變數必須是 Immutable ,用 map reduce 取代傳統的 for loop 和 while,但常常就只是說這樣寫比較漂亮,卻很難表達說這樣寫有怎樣的好處,這篇文章記錄一下我對這些概念的小小心得。
Pure function 與 side effect #
在 functional programing 中 funtion 的 side effect 指的是除了這個 function 的回傳值以外,function 的執行會影響其他地方的程式狀態,pure function 指的就是這個 function 沒有任何的 side effect。
function notPure(paramsA) {
if (condition) {
changeOtherStateBy(paramsA)
}
...
}
function pure(paramsB) {
return dataTransform(paramsB)
}
所以當你的 function 絕大部分都是 pure function 的時候,function 的實作只要關心 return 的正確性,除了容易測試以外,而且不用擔心這個 function 為了保持外部狀態邏輯而必須確保調用的順序以及次數正確,程式的狀態也會更少了些。
Immutable 的好處 #
Immutable 的意思是指這個變數的值會在一開始初始化以後在 runtime 就不會再被改變。所以下列這些寫法都不是 Immutable
let i = 'foo'
if ( blablabla... ) {
i = 'bar'
}
const b = {}
b.foo = 'bar'
Immutable 在平行化程式避免 race condition 其實有相當大的好處,例如 Mozilla 開發的 Rust 語言就有實踐這樣的原則,但是畢竟 JavaScript 是 single thread 的執行,從平行化的角度去講好像說服力也沒有很大,況且 JavaScript 並沒有天生自帶完全的 Immutable 的能力,就算使用了 const,依舊可以對 object property assign 新值,亦或是可以 push 新值到 array 裡面,另一方面 React.js 注重 Immutable 的原因是因為他必須用一個簡單高效的方式去比較前後的 props 來決定重新渲染的必要性,當 props 是 immutable 的時候你就可以相當容易的用 shallow equal 比較出來,那專案其他地方 Immutable 的必要性為何?所以我們來逆向思考好了,與其問 Immutable 有什麼好處,不如來思考傳統的 mutable 寫法到底會有什麼樣的問題。
Mutable 的問題 #
一個 mutable 變數就意味著這個變數在 runtime 可以有多個值,而你要了解某一行這個變數的值是多少,取決於從這個變數的宣告到當下的中間程式碼對他修改的次數跟順序,當程式碼越寫越多,變數經過一個又一個的 function 有的時候 call by value 有的時候又是 call by reference,其實你很難一眼就可以看出這個變數修改的次數跟順序有沒有符合你的預期,程式碼的閱讀成本其實是會增加的。而 Immutable 的寫法就讓變數的值只會在宣告的時候決定然後就不會再變動,code 你只要 trace 當初宣告的地方,就不用怕中間的細節會影響。
map reduce vs for loop while forEach #
減少多餘的 counter 變數跟終止判斷 #
如果你希望遍歷 array 裡面所有的 item,傳統的作法離不開 while for loop 的迴圈慨念,會利用一個 counter 變數來做為判斷迴圈終止的依據,後來出現的 forEach map reduce,讓你少宣告了這個可變的 counter 變數,不需要多處理 counter 邏輯,就可以讓遍歷這件事情可以被顯而易見的方式表達出來。
表達力的差別 #
遍歷 item 常見的情境是要轉換不同的資料結構,在這種的情境下 map 跟 reduce 會比 forEach 更適合,forEach 的表達能力只能告訴讀者說,這裡會遍歷所有的元素,然而 map 跟 reduce 還會告訴你這裡的回傳值會是一個 array 或是一個 accumulate 的資料,同時也實現了 immutable 的功能。
用第三方 library 來補足原生的 method 的不足 #
雖然 ECMAScript 5 以後,Array.prototype 本身就支援 map reduce filter slice 等這些功能,但仍有其不足之處:
- 如果調用處不是 Array,runtime 就會噴出錯誤。常常發生在 api 在空值或是錯誤的處理上會給出無法預期的欄位型別,這個部分常常必須前端額外處理。
- Object 本身沒有支援類似的功能
因此我個人還蠻推薦用第三方 library 如 lodash 來取代原生的 method,好處是:
- null undefined 的 input 處理
- 有其他比 reduce 更強的表達力針對特定用途的 method,例如 flatten
- 對 Object 也有提供 pickBy omitBy mapValues 等相對應的 method
- 實作了許多底層上性能的最佳化
_.map(undefined, item => item + 1) // \[\]
_.flatten(\[1, \[2, 3\], \[4\]\]) // \[1, 2, 3, 4\]
_.pickBy({ a: 1, b: 2 }, value => value > 1 ) // { b: 2 }
_.omitBy({ a: 1, b: 2 }, value => value === 2) // { a: 1 }
_.mapValues({ a: 1, b: 2 }, value => value \* 2) // { a: 2, b: 4 }
結語 #
的確在實作這些原則的時候,會犧牲一點點效能,多浪費一點點記憶體,但是在現代 JS 的引擎的黑魔法跟黑科技 CPU 的加持以及記憶體越來越多的狀況下,程式碼上的 micro optimizations 其實影響沒有那麼大,特別是在 UI 上只要在 100ms 之內完成響應對使用者其實沒有太大的差別。UI 的開發效率跟正確性常常比 UI 的效能更加的重要。
其實這些原則想要達成的目的概括的來說就是:
- 讓邏輯變更小更容易測試
- 減少達成目的所需要的程式狀態數量
- 更動程式狀態要用簡單且容易察覺的方式來做
- 減少不必要的程式的執行順序以及調用次數和細節的思考
- 降低程式碼的閱讀成本,快速的讓 Reviewer 知道你要做什麼,而且不用擔心有問題
當你達到這些目的就輕鬆的達成軟體設計中所講的 KISS 原則
KISS principle: Keep it simple and stupid
當然你也可以說傳統 imperative 寫法即使不遵守這樣的原則也可以達到同樣的目的,程式碼也很簡單阿,只是增加一個 flag 變數做判斷或者只是單純跑個迴圈,根本不會有像我舉的例子會出現的那些問題或是沒那麼嚴重。
但一個專案應該要有一個如何構建整個問題跟解決方法的模型,不會因為不同的 case 而改變,所以與其說這些是原則,不如說是一種程式方法論(或者說是宗教?)上的具體實踐,用 declarative 的框架享受 declarative 的好處,但是在其他地方可以輕鬆的選擇用 declarative 的原則實作功能的時候,卻毫無理由的使用 imperative 的方式處理 data,就會有一種你只是為了框架而使用框架,而不是真正認知到這個編程範式方法論所帶來的好處。
當然這不是一個教條式的規範,也不是說所有不遵照這種作法都是 anti pattern,軟體設計並沒有所謂的銀彈或者是解決問題的萬靈丹,也許是因為這邊是效能的瓶頸所以我想要用 imperative 的方式增加一點效能,又或者是這樣寫對各平台的兼容最好,那些選擇的背後應該是可以去思考具體的理由,而不只是一個這樣寫也可以的個人習慣。