一般而言,在業界主張要使用函數式編程 (FP) 的理由主要有兩個:
另一方面,反對使用函數式編程的理由主要也有兩個:
在我來看的話:「開發速度快」與「人不好找、不好訓練、可維護性問題」這兩個理由相加總,最後的結果跟外在環境有關。它牽涉到外在環境、與商業決定,而不是純粹的技術討論。
比方說,一個人會用 FP ,所以開發速度快。但是,跟這個人比較的是,一個不會用 FP 的團隊時,個體雖然慢一點,但是總和而言的速度卻更快。又或是說,我們把「個人的開發快」看成是開發的人力成本低。那如果我們把軟體轉移到第三世界國家去開發,而第三世界國家都是使用 imperative programming ,他們的成本還是更低啊。
綜合以上,「FP 可以讓開發速度快」確實有可能跟「FP 的人不好找」這個產業現況相抵消;於是,要不要使用 FP 的主要考量就會變成了三項:
不知道讀者有沒有覺得「減少 bug」與「付出機器的效能做為代價」這兩個同時出現很眼熟?沒有錯,這個就是典型的高階語意解決方案常常導致的現象。
想像一下,你需要記帳,但是「你只需要知道每天正確的總餘額是多少」,並不需要像一般的公司一樣製造財報。
日期 | 項目 | 收入 (+) | 支出 (-) | 備註 |
---|---|---|---|---|
9/1 | 薪資 | + $35,000 | 薪水入帳 | |
9/2 | 早餐 | - $65 | 買三明治和咖啡 | |
9/3 | 繳房租 | - $15,000 | 9月房租 | |
9/4 | 晚餐 | - $350 | 朋友聚餐 | |
9/5 | 購買生活用品 | - $850 | 洗衣精、牙膏等 |
這樣子的話,你至少有兩種方式來達成目標:
你會定義一個變數 balance
,然後透過一系列的賦值操作來改變它的狀態:
- `balance = 0` (初始值)
- `balance = balance + 35000` (薪水)
- `balance = balance - 65` (早餐)
- `balance = balance - 15000` (房租)
用這個方式的話,一定超級省記憶體、計算也很簡單。也是可以達成目標:知道每天正確的總餘額是多少。附帶一提,前述「依序改變狀態」的方式就是命令式編程的特色,某種程度來講,這是一項 feature ,因為很省記憶體。
那麼,如果用函數式編程 (FP) 的方式來記帳會是什麼樣子呢?
FP 不是透過一再地修改既有的狀態達成運算,而是把「計算餘額」這件事看作一個函數。你會把所有的交易記錄當成這個函數的輸入 (input),然後函數會給你一個輸出 (output),也就是最終的餘額。它儘量減少不會去「改變」狀態,而是一再地去「計算」出新的結果。
例如,你可以想像一個 sum
函數,它的輸入是一個包含所有交易的列表,輸出是最終的餘額:
balance = sum([+35000, -65, -15000, -350, -850])
這邊注意一下這個計算方式,一方面,你就需要記錄下每一筆的交易、都不能丟棄、顯然多消耗了許多的記憶體。另一方面,sum
這個函數,它的結果 (output) 只跟輸入有關,跟任何外在的狀態都無關。
讀到這裡,你可能會想:「這兩種方式都能得到最終餘額,那 FP 到底有什麼好處?」
假設今天出了個差錯,你發現最終的結果算錯了?
如果你的記帳是使用命令式編程,典型的除錯方式,你要在 balance 每一次改變值的時候,都去檢查,它的新值是否正確。換言之,你要不停地去檢查系統內部的狀態。
但如果你的記帳是使用函數式編程,典型的除錯方式,你要去確保 sum 這個函數的資料轉換是否總是正確,還有給予 sum 的輸入是否正確。注意到了嗎?相對於命令式編程的除錯方式,函數式編程巧妙地讓讓除錯的複雜度拆成兩個區域:資料轉換與輸入。
就是上述的這個分而治之 (divide and conquer) 的可能性,讓除錯可以大幅地簡化。
函數式編程之所以被認為是一種高階語意,正是因為它把解決問題的複雜性從「如何一步步改變狀態」提升到「如何定義純粹的函數來計算結果」。
這種抽象化帶來的好處是顯而易見的:你的程式碼因為沒有副作用而更不容易出錯,也更容易進行測試和除錯。
但是,這種抽象化並非沒有代價。你為了避免中間狀態的複雜性,必須儲存更多的資訊,並在需要時重新計算,這通常會消耗更多的系統資源。
所以,FP 是一種典型的高階語意解決方案,它用更高的抽象層次來解決問題,最終讓你得到: