這些年來,Functional Programming (FP,函數式程式設計) 受到愈來愈多開發者青睞,成為不少新世代程式設計的首選,OOP (Object Oriented Programming 物件導向程式開發) 稱霸的榮景不再。

最震驚的莫過於 C++/Java/C# 開發者們,打從程式語言學步階段就不斷被灌輸:要以物件為中心,貫徹封裝、抽象、繼承與多型精神,實現高模組化、可重用與可擴展的程式結構,才能做出偉大的系統。結果幾十年過去,一眨眼世界變了,彷彿 FP 才是王道。

在 Java 領域有 Scala、在 .NET 則有 F#,加上這些年被呼籲用以取代 C/C++,講求速度與安全的 Rust,都是原生支援 FP 的代表。而即使是 Python/C#/JavaScript/Java 等傳統偏向 OOP 的程式語言,也紛紛加入 FP 思維的設計,例如: Lambda Expression、map/reduce/filter、不可變容器... FP 已成顯學。

練拳一輩子的老武師,猛一抬頭,發現大家都在踢跆拳道... 登楞!

thumbnail

為了解答這個疑問,我花了點時間研究這股趨勢的成因,試著了解為何 FP 會被狂推。

FP 之所以能取代 OOP,自然是瞄準了 OOP 的痛點,會如此發展則與時代演進有關。

其實 FP 語言的歷史很早,1930年代數學家 Alonzo Church 提出了 λ 演算(Lambda Calculus),是一種用「函數抽象與套用」來表達計算的純數學模型(所以 Lambda 這個名字是這麼來的,長知識了)。1950–60 年代電腦出現後,數學家試著把 λ 演算思想變成程式語言,LISP (1958)是第一個成功的函數式語言,但後來命令式 / 結構化程式設計 (如 C、Pascal、Fortran) 因為更符合 CPU 的運算特性,成為程式語言的主流,此段時間 FP 只能默默發展,期間出現 ML (1973)、Scheme (1975)、Miranda (1985) 等語言,學界推出的 Haskell (1990) 成為「純函數式語言」的代表。

1990 年後展開 Internet、雲端時代,軟體系統日益複雜,為應付高流量情境需要大量並行運算,傳統命令式的 OOP 設計,在多執行緒環境需解決資料存取衝突問題,開發難度驟增。另一方面,企業級與雲端系統講求可測試、可組合、可推理性,使用 OOP 實踐也會遇上不少困難。

OOP 主打封裝資料及行為,保存狀態並集中商業邏輯,藉此降低耦合度與提升可維護性,有利於大型專案分工與邊界定義。然而,隨著分散式架構、多核平行運算成為日常,共享可變狀態在高並行情境很容易衍生同步化與 Race Condition 問題,常需複雜的額外鎖定與協議控制克服。在高並行情境,處理這類問題的複雜度常超乎想像,於是講求資料不可變的 FP 成了解藥。

FP 的主要特色如下:(註:我試著補上 C# 的 FP 實踐範例方便理解)

  • 純函數(Pure Functions):效法數學函式 (例如:f(x) = x * 2) 精神,相同輸入必得相同輸出,不產生任何副作用(不修改外部狀態、不做 I/O 動作),易於推理與測試。
    例:Func<int, int, int> add = (a, b) => a + b;,add(1, 2) 只會等於 3,沒有其他可能。
  • 不可變(Immutability):要求資料不可變,不共享可變狀態,如此能大幅降低並行情境的 Race Condition 與同步成本。
    C# 9推出了 Record 用來封裝不可變資料(Immutable Data),主要也在呼應 FP 應用。
  • 高階函數(Higher-Order):函數是一等公民,可作為參數或回傳值,支持組合應用與抽象化。
    Lambda 當參數用在 LINQ 已是日常:Func<int, bool> ge5 = (i) => i >= 5; (new [] { 1,2,3,4,5,6,7 }).Where(ge5);
  • 遞迴與組合(Recursion & Composition):以小函數組合大功能,透過函數串接合成取代命令式流程控制。
    LINQ 寫法是最簡單的實例:collection.Where(o => o.Count > 3).OrderBy(o => o.Price)...
  • 強型別與型別推導(Type Systems):使用嚴謹強型別系統與推導,在編譯期即可攔截型別錯誤。
  • 宣告式(Declarative):描述要算什麼而非怎麼算,貼近數學定義,便於等式推導與重構。
    var add = int (int a, int b) => a + b;(C# 10+ Lambda 可明確宣告傳回型別,用 var 取代 Func<int, int, int>),運算邏輯類似數學函式,一望便知。
  • 惰性求值(Lazy Evaluation):要用到時才評估計算,提高效率並方便彈性組合。
    LINQ 的延遲執行便是一例,忘記了小心踩坑。

而這帶來一些好處:

  • 適合平行處理:由於不共享可變狀態,不用擔心平行處理時程式打架,Thread-Safe 渾然天成!
  • 測試超級好寫:純函數保證輸入傳什麼不可變資料一定得到什麼輸出,自動測試不需動用複雜 Mocking 模擬物件行為及狀態。
  • 易於了解推理:宣告式特性讓邏輯透明,重構與驗證行為更直觀。

在講求高並行性大量平行處理的場景,FP 的特色與好處格外珍貴,於是在金融、電信、雲原生、大數據、人工智慧等領域,許多人會選擇 FP 語言取代傳統 OOP 架構。

至此,我們對 OOP 的缺點與 FP 的好處已有粗淺認識。不過,OOP 與 FP 倒也不是非黑即白漢賊不兩位。傳統 OOP 語言如 Python、C#、JavaScript/TypeScript、Java 在傳統 OOP 主軸外,也開始支援 FP 概念 (最簡單鑑別方法可看是否支援 Lambda 寫法),把程式寫成 FP 風格絕對沒問題。而以 FP 為主的 Scala/Kotlin/F#/OCaml/Swift/Rust 也加減能支援 OOP 的部分特性(不過對於「繼承」,有些語言倒是傾向不支援,例如:Rust、Golang)。

從學術一點的角度來看,OOP 或 FP 可視為程式設計的兩種典範(Paradigm),而所謂的 Paradigm(典範/範式)指的是一個特定時期內,一個群體(如科學社群、學術領域)所認同的基本世界觀、信念、價值觀或方法論,為該領域的成員提供一個共同遵循的模式和模型,一套被廣泛接受的思維方式或研究框架,指導人們如何觀察、思考、提問和解決問題。

科學哲學家托馬斯·S·庫恩提出「典範轉移(Paradigm Shift)」的概念,指一個舊典範不再能解釋現有現象或被新發現取代時,會發生一種根本性的變革或科學革命,整個研究群體的思維方式和方法論隨之改變。例如,從地平說走向地圓說。

在程式設計領域,從最早一路到底的程序式寫法到結構化設計,從結構化到 OOP,再從 OOP 到 FP,這些演進涉及對問題解決方式的重新思考,例如,從以動作為中心轉為以物件為中心,便可視為一種典範轉移。FP 強調不可變性、純函數與數學函數評估,而 OOP 著重封裝、繼承與多型,兩種方法對於狀態管理、程式結構有著截然不同的設計哲學,絕對也是一種典範轉移。

不過程式開發領域的典範轉移,不同於庫恩聚焦的單一性典範(例如:地平說與地圓說只能二選一),「多典範共存」是常態。OOP 物件邊界清晰,能直覺化映對領域實體,易於思考設計,繼承、多型與介面則有利於結構化重用與替換,在複雜領域模型、長生命周期、替換與擴展需求強烈情境仍有壓倒性優勢。當可維護擴充性比高並行、可測試性更重要,堅持用 FP 硬幹便不是個好主意。目前的發展也走向二者共存,像是大部分 OOP 語言都能實現 FP (寫過 LINQ 就等於有在用 FP 了),而 FP 語言也會實做部分 OOP 功能。

一個可行策略是「大範圍 OOP,小範圍 FP」的混合策略,在高層次介面使用物件導向設計維持低耦合易於維護擴充,邏輯實作則視需求採用函數技術滿足高並行需求,OOP 與 FP 摻在一起做成瀨尿牛丸,應該是最好的做法吧。

thumbnail

【延伸閱讀 for .NET 開發者】

This article explain why functional programming offers safer concurrency and easier testing by emphasizing immutability, pure functions, and declarative code—making it popular for modern, scalable systems beyond OOP with C# exmaples.


Comments

# by cash

FP 的話大推這本,我覺得很淺顯易懂,缺點是太大本了 XD https://www.tenlong.com.tw/products/9789863128090

# by YT

是Race Condition嗎?

# by yoyo

OOP常看到有那種明明不是繼承關係的,卻實作成繼承,全部耦合在一起難以修改 例如:多個Web API Request / Response 有些重複的欄位用繼承放一起.. "Favor composition over inheritance" 大多數情況應該用組合而非繼承 OOP適合用在實體物件抽象化,不同的Paradigm,有不同的適合場景

# by Jeffrey

to YT, Yes, 打錯囉~ (錯字人正常發揮)

# by ChrisTorng

我 2021 年有研究過 F#,對於 C# 能手而言,強烈建議試著學習看看! 我是由 https://fsharpforfunandprofit.com/ 學習的,官網應該也不錯 https://learn.microsoft.com/zh-tw/dotnet/fsharp/what-is-fsharp 。有很多 functional 的招式,真是大出 OOP 人的意料之外,沒想到一個 function 可以玩出這麼多花樣。 不過覺得為了保持純粹,不能有副作用,需要額外動腦筋,相較 OOP 就覺得麻煩。另若導入 F#,對一起工作的團隊也是很大的額外負擔,最後還是選擇在 C# 中多導入 FP 的寫法。 我把研究結果整理在 https://github.com/ChrisTorng/FSharpLearning ,裡面大量使用 .dib,就是 https://blog.darkthread.net/blog/dotnet-interactive/ 中介紹的 Polyglot Notebooks。 在其中 3. Domain 資料夾內有我當時如何使用 C# 來撰寫 FP 樣式程式的範例。 不過後來 C# 新版陸續有導入更多功能,像我學過 F# 的人就會覺得很熟悉,認出它們就是 FP 所需要的特性。 雖然我沒有真的把 F# 學起來,沒有真正用它寫什麼程式,但寫 C# 時,會採用 F# 中的一些方法,讓程式更純粹,容易理解及測試。

Post a comment