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!

對抗久坐職業傷害

研究指出每天增加 2 小時坐著的時間,會增加大腸癌、心臟疾病、肺癌的風險,也造成肩頸、腰背疼痛等常見問題。

然而對抗這些問題,卻只需要工作時定期休息跟伸展身體即可!

你想輕鬆改變現狀嗎?試試看我們的 PomodoRoll 番茄鐘吧! PomodoRoll 番茄鐘會根據你所設定的專注時間,定期建議你 1 項辦公族適用的伸展運動,幫助你打敗久坐所帶來的傷害!

贊助我們的創作

看完這篇文章了嗎? 休息一下,喝杯咖啡吧!

如果你覺得 MyApollo 有讓你獲得實用的資訊,希望能看到更多的技術分享,邀請你贊助我們一杯咖啡,讓我們有更多的動力與精力繼續提供高品質的文章,感謝你的支持!