前些日子我寫了篇關於 functional programming 的文章 (Functional Programming 一文到底全紀錄),講述了當時對 FP 的學習心得,與在實務中應用後的一些想法。
之後陸陸續續收到了一些反饋,有人詢問:「FP 能做的事情,現在 OOP 都做得很好,什麼情況下需要用到 FP?」
關於這個問題,我的回答總是 FP 準則中的「避免副作用」、「一個 Function 只做一件事情」、「以 Function 為程式的最小單位」能使我們只需要關注 Function 的正確性即可,讓程式更佳可讀、更易維護。
Point Free Style
如果你用中文搜尋 point free 相關的網站,可能會發現資料少得可憐,比較有系統的大概就是阮一峰的一篇文章,我很推薦他的這篇,淺顯易懂的將 point free 的核心觀念帶出來。
那什麼是 point free style 呢?
用我的話來說,我會這樣解釋
point free style 是 FP 的應用;我們使用各種事先定義好的 function 組合出我們要做的事情,這些 function 都不牽涉到數據的處理。
數據流與程式行為分離開來,使我們只關注在做什麼,而非怎麼做(如何處理數據)。
簡單的說就是:「point free 沒有再跟你說資料長怎樣的。」
程式的行為怎麼可能不看數據做事?
這聽起來很玄幻,但是藉由 FP 的觀念,一步一步的 Refactor 我們的 Code 是可以做到的。
舉一個小小的例子作為前菜
我們首先定義一些通用的 function
假設我想從公司人員列表中找出員工的名單:
如此,當我的人員列表傳入 getEmployee
時,我便可以自動從中找出 role === 'employee'
的人員。
const data = [ {name: "小明", role: "employee"}, {name: "小華", role: "employee"}, {name: "小英", role: "employee"}, {name: "小藍", role: "employee"}, {name: "大板", role: "admin"} ];getEmployee(data); // [ // {name: "小明", role: "employee"}, // {name: "小華", role: "employee"}, // {name: "小英", role: "employee"}, // {name: "小藍", role: "employee"} // ]
看到了嗎? 我們在撰寫程式時,並不管 data 的資料型態與結構,一切等到資料來了,丟進去 function 即可。
這樣有什麼好處?
我們看看 getEmployee
const getEmployee = filter(
pipe(
propRole, // 取得公司人員的 `role`
isEmployee // 檢查是否是員工
)
);
淺顯易懂、清晰直白。
利用 point free ,藉由將 data flow 抽離出來,我們可以更加關注在程式本身想表達的含意中。
如何以 point free 方式思考
我們舉一個比較貼近實務可能會碰到的例子。例如公司要計算某產品 A 當年度 (2019年 01 月 01 日 ~ 12 月 31 日) 的總銷售量,資料如下:
var data = [
{ date: "2018-01-01", item: "A", amount: 5 },
{ date: "2019-01-11", item: "B", amount: 10 },
{ date: "2018-02-05", item: "C", amount: 3 },
{ date: "2019-03-21", item: "A", amount: 1 },
{ date: "2018-04-18", item: "B", amount: 522 },
{ date: "2017-01-01", item: "C", amount: 51 },
{ date: "2016-01-13", item: "A", amount: 4 },
{ date: "2019-01-18", item: "A", amount: 345 },
{ date: "2018-12-18", item: "B", amount: 7 },
{ date: "2019-11-24", item: "B", amount: 64 },
{ date: "2019-07-15", item: "C", amount: 22 },
{ date: "2019-06-25", item: "A", amount: 546 },
{ date: "2019-04-04", item: "C", amount: 234 },
{ date: "2019-05-07", item: "B", amount: 1111 },
{ date: "2019-07-15", item: "A", amount: 236 },
{ date: "2019-10-16", item: "B", amount: 81 },
{ date: "2019-11-20", item: "A", amount: 90 },
];
依照一般程式撰寫邏輯,可能會寫成這樣
這個寫法也沒什麼不好,它很忠實地完成了我們需要的任務。但是我們無法一眼就看出這段 Code 在做什麼,我們需要去讀裡面的程式邏輯,才能進一步知道這是在計算 A 產品的當年度營銷總數。
另外,由於 total 這個變數在迴圈外被定義,但是更動卻被隱藏在迴圈當中,我們難以一眼就將焦點關注到 total — 這段 code 的結果(變數的隱匿性)。這也造成程式難以追蹤跟維護(具有副作用)。
因此下一步,我們利用 FP 的觀念將程式重構。
重構完後,我們一眼就可以知道 total 是我們最終求得的結果,並且我們可以很清楚地知道程式流程為
- filter 找出在此年度(isInYear)與是Item A (isItemA)的資料
- 擷取出資料的
amount
- 加總後得到我們要的結果。
到這一步,其實程式也已經具有很高的可讀性了。唯一的遺憾是在這段,我們需要費一點功夫才能知道它想表達的是什麼。
data
.filter(row => isInYear(row) && isItemA(row))
.map(getAmount).reduce(sum, 0);
若是以 point free 來改寫會變成什麼樣子呢?
我們利用上方已經定義好地 General function,如此就不用重新定義了。
首先我們將剛剛的程式邏輯中,碰到資料處理的部分細拆出來。
接下來,我們就可以利用這些 function 組合出我們要的 get_item_A_in_year
了。
我們可以看到 getItemAInYear
並沒有依賴任何 data 的資料結構,我們完全關注在它要做什麼事情。
但是這種寫法其實看得也是挺痛苦的,所以我們可以再進一步優化,將上面的邏輯再抽象出來。
最終,我們利用上方定義好的各種 function ,組合出 getItemAInYear
const getItemAInYear = pipe( filterIsItemAInYear, // 過濾出在此年的 Item A getAmount, // 獲得數量 sumTotal // 加總);var total = getItemAInYear(data); // total = 1218
到這步,有沒有覺得比起最初單純使用 for loop 的程式更加地直觀,且易讀又乾淨呢?
順便附上完整的程式碼
總結
point free style 的程式方式,是將 functional programming 的精神貫徹的更加徹底。
從上方兩個例子,我們可以很清楚發現,point free 真正做到「function 為最小單位」、「一個 function 只做一件事情」,將 FP 的程式撰寫精神最大化。
我個人認為 functional programming 的思維方式是需要訓練的。能否靈活地使用 point free 解決問題,體現出撰寫者對 FP 的熟悉程度。
命令式的程式撰寫可以很容易地解決問題;但是聲明式的程式撰寫風格除了將問題解決之外,更讓程式變得易讀、易維護,並精準地傳達撰寫者的思想。
由此可知,同樣的一件事情從 FP 的角度思考,與命令式程式那樣地平直思考,兩者的程式風格便有著天壤之別。
至於哪種方式更讓人喜愛或接受,就看各位怎麼看待了。
另外在看完上面的例子之後,應該會有不少人人產生「我只是想要做一個簡單的事情,卻要搞這麼多 Function,將事情變得這麼複雜」這樣的想法。
在 NPM 上有一個 Library 叫做 Ramda ,這是一套專為 Javascript Functional Programming 設計的 General Library。只要 Import 這套 Library 之後,我們就不用再額外自己手刻 general function 囉!