Functional Programming 之美 — 概念篇
Posted on Jan 25, 2024 in Functional Programming by Amo Chen ‐ 4 min read
有人的地方就有江湖,程式設計的風格也有流派之分,其中有 1 派提倡程式設計應該要像數學函數一樣美,輸入什麼就輸出什麼,過程之中不應該有 side effect 甚至是修改外部 state (例如變數)存在,換句話說, Functional Programming 在追求的就是最純粹的函數(pure function) 。
函式應該要像數學公式一樣純粹,這意味著它們不應有副作用(side effects),也不應修改任何外部狀態
Side effect 有很多種情況,簡單來說任何使用非函式內部的變數,或者修改非函式內部的狀態就算 side effect, 這種會使得函式的表現受到外部干擾而改變行為,或者函式會去干擾其他事物的行為,都屬於 side effect 。
下列寫法是 1 個經典的 side effect 例子(以 Python 為例):
global_factor = 2
def adjust_value(n):
global global_factor
return n * global_factor
當我們呼叫 adjust_value(1)
就會得到 2, 一旦程式有其他改了 global_factor, adjust_value(1)
的行為就會發生改變:
adjust_value(1) # 2
global_factor = 3
adjust_value(1) # 3
還記得「輸入什麼就輸出什麼」的原則嗎?上述情況就使得 adjust_value()
不符合輸入什麼就輸出什麼的原則,使得它日後可能變成 bug 的來源,所以 functional programming 認為應該要消除 side effect 。
再看 1 個例子,以下是修改 state 的範例:
mapping = {
'a': 1,
'b': 2,
}
def del_k(d, k):
del d[k]
上述範例同樣不符合「輸入什麼就輸出什麼」的原則之外,也造成函式會去干擾其他事物的行為,如果執行以下程式,就會發現問題,原因在於 mapping
一直被 del_k()
給改變,導致程式不可預期的行為產生:
>>> for k in mapping:
... del_k(mapping , k)
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: dictionary changed size during iteration
對於 Functional Programming 流派來說,這種修改外部 state 也要被消除!因為它就是 1 種 side effect !
所以最後 del_k()
可能會是類似以下的寫法,過程中沒有任何外部 state 受到傷害:
mapping = {
'a': 1,
'b': 2,
}
def del_k(d, k):
return {_k: _v for _k, _v in d.items() if _k != k}
由於不能修改外部 state 的原則,所以我們改使用 Python 的 Dictionary Comprehension 走訪傳進來的字典,跳過要刪除的 key, 剩餘的 key 都複製到 1 個新的字典後回傳。
再次執行下列程式的話,也不會有任何問題:
>>> for k in mapping:
... del_k(mapping , k)
不過這個範例比較不切實際,實際上要刪除 dictionary 所有 key 的話,其實可以直接 assign 1 個新 dictionary 就好,前述範例只是為了讓人知道修改外部 state 的可能壞處,藉此體驗一下 functional programming 世界的美好。
目前為止,我們已經學了解 functional programming 1 個重要原則:
「不要有 side effects」
這個原則不僅僅只適用 functional programming, 其他程式設計風格也十分受用,其原因在於:
幫助我們寫出更好測試的程式
由於函式是純粹的函式,其輸入與輸出的一致性使得單元測試更容易實作。因為可以預測函式行為而無需擔心外部環境的影響,這讓測試變得更為簡單且可靠。
適合需要平行處理的程式開發
在修改外部狀態的情況下,特別容易在平行處理時造成需要額外處理的鎖(lock)與效能問題。原因在於多個 processes/threads 可能共同存取同一個外部狀態,這就導致我們需要使用 lock 確保結果的正確性。而一旦使用鎖,就一定會對效能產生影響。因此,由於 functional programming 鼓勵使用 immutable data 和純函式,這降低了在並行環境中處理共享狀態時出現問題的風險,從而使得程式的平行處理更為安全和高效。
不過 functional programming 也並非完美無缺,以下是其常被詬病的缺點:
學習曲線高
functional programming 的概念和風格可能對於那些不熟悉的開發者來說是新的挑戰。學習如何使用高階函數、避免修改狀態等新概念可能需要一些時間適應。
效能問題
在某些情況下, functional programming 的效能可能不佳。這是因為 functional programming 通常會產生一些額外的開銷,例如遞迴可能導致一些效能上的挑戰。
不易讀寫
一些開發者認為 functional programming 風格難以理解,這可能使程式碼可讀性降低。原因也很簡單,為了符合 functional programming 的原則,你只能使用更多純粹的函式組合出符合原則的程式碼,這使得有些很直觀的做法變得很不直觀、難以理解。
不適用於所有情境
functional programming 不一定適用於所有情境。在一些需要直接修改狀態的情況下,堅持使用 functional programming 就相當不實際。
記憶體使用量相對高
functional programming 通常需要更多的記憶體,因為它通常使用 immutable 的資料結構,這導致相對較高的記憶體使用。在需要修改數據時, immutable 導致需要複製資料,而不是直接修改現有資料,如同本文第 2 個範例,它使用複製 1 個新的 dictionary 的做法取代直接修改該 dictionary, 這就導致記憶體在某個時間點內需要使用接近 2 份相同的 dictionary 的容量,這可能導致不適合對記憶體容量受限制的應用使用。
總結
總的來說, functional programming 也是個好的程式設計風格,不過也有其適合使用的情境,別貿然一味的使用 functional programming, 否則容易搬石頭砸自己的腳。
而其實 functional programming 還有很多模式/方法可以介紹,有一些甚至已經是我們日常就在使用的技巧,我們將在日後的系列文章中介紹。
以上!
Enjoy!