前些日子我寫了篇關於 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 的程式更加地直觀,且易讀又乾淨呢?
順便附上完整的程式碼
![](https://miro.medium.com/max/45/1*itAJHMwn0D-DpWte_sFMxA.png?q=20)
總結
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 囉!