跳轉到

2020s-函數式思想的全面內建#

案件摘要#

到了 2010 年代末與 2020 年代,函數式思想完成了一件百年運動的終極目標:它不再需要「函數式語言」這個專屬載體——它被內建進所有主流語言。Rust 的 Option/Result 與 iterator、Swift 的 enum 加 pattern matching、TypeScript 的 discriminated union、Kotlin 的 data class、Java 的 Stream(2014)、C++20 的 ranges、Scala 3(2021),全是 λ-Calculus 的後裔。與此同時,真正的函數式語言也攻入工業要塞:Clojure 進入拉丁美洲最大數位銀行 Nubank(2021 年收購 Cognitect)、Haskell 進入區塊鏈 Cardano/Plutus、OCaml 進入 Tezos/Michelson、Futhark(2017)把純函數式程式編譯到 GPU。本案要偵破的謎題是:為什麼是「不可變」與「純函數」這兩個看似高昂的代價,贏下了多核與平行時代?

前因 -- 為什麼會有這個案子#

  • 2005 年前後,摩爾定律的頻率紅利見頂,處理器轉向「加核數」:多核(multicore)與眾核(many-core)成為常態,單執行緒效能不再免費成長。
  • 共享可變狀態成為效能與正確性的共同敵人:鎖(lock)既拖慢速度又引入死鎖;data race 是不可重現的 bug,除錯成本無上限。
  • 非同步回呼地獄(callback hell):Node.js 與瀏覽器的事件迴圈中,巢狀回呼層層堆疊,控制流與錯誤處理支離破碎。
  • 兩個陣營的實證判決:Haskell 與 Rust 用 immutability 與型別安全證明——「不可變」不只更安全,在平行場景還更快,因為不需要鎖。
  • 學院理論早已備齊:Chris Okasaki 1998 年的《Purely Functional Data Structures》證明持久化資料結構可以高效;線性型別(linear types)在 1987 年由 Girard 提出,等待被工程化。

線索與推理 -- 數學式、程式、理論#

線索一:Option/Result monad 的內建——? 就是 bind#

Rust 把 Maybe monad 直接做成標準庫型別:

\[ \texttt{enum Option<T> \{ Some(T), None \}}, \qquad \texttt{enum Result<T, E> \{ Ok(T), Err(E) \}} \]

傳統函數式語言用 >>=(bind)鏈式傳播 Maybe:

\[ (>>=) :: \text{Maybe } a \rightarrow (a \rightarrow \text{Maybe } b) \rightarrow \text{Maybe } b \]

Rust 用一個 ? 運算子做同一件事:遇到 None 或 Err 就提前返回,否則取出值繼續。語法糖底下是同一個 monad 定律:

fn parse_config(path: &str) -> Result<u32, String> {
    let text = std::fs::read_to_string(path)?;      // Err 就提前返回
    let line = text.lines().next()
        .ok_or("empty file".to_string())?;           // None 轉 Err 再傳播
    let value = line.trim().parse::<u32>()
        .map_err(|e| e.to_string())?;                // bind 的語法糖
    Ok(value)
}
// 每一行 ? 都是 >>=:鏈式錯誤傳播,錯誤路徑一目了然

Haskell 的 do notation、Rust 的 ?、Swift 的 try,全是同一個 monad 在不同語言裡的內建化身——錯誤處理從「散落的 if 檢查」收斂成「型別與運算子的代數」。

線索二:持久化資料結構與結構共享#

不可變聽起來要每次複製整份資料,代價 O(n)。Okasaki 的持久化資料結構(persistent data structure)用結構共享(structural sharing)破解:修改時只複製路徑上 O(log n) 個節點,其餘節點與舊版本共享。

以 persistent vector(Clojure 的核心結構,基於 32 分叉樹)為例:

\[ \text{update}(v, i, x) : \text{成本 } O(\log_{32} n), \quad \text{共享節點數 } n - O(\log_{32} n) \]

用 Python 模擬這個結構共享思想:

class Node:
    def __init__(self, children):
        self.children = children   # 不可變:建好後不再修改

def update(node, path, value):
    # 沿路徑只複製 O(log n) 個節點,其餘與舊版共享
    if not path:
        return value
    i = path[0]
    new_children = list(node.children)          # 淺複製:引用共享
    new_children[i] = update(new_children[i], path[1:], value)
    return Node(new_children)

# 32-ary tree,內含 3 層:root -> branch -> leaf
leaf_a = Node([1, 2, 3])
leaf_b = Node([4, 5, 6])
branch = Node([leaf_a, leaf_b])
root_v1 = Node([branch])

root_v2 = update(root_v1, [0, 0, 1], 99)   # 改 leaf_a 的第 2 個元素

print(root_v2.children[0].children[1] is root_v1.children[0].children[1])  # True:leaf_b 完全共享
print(root_v2.children[0].children[0].children[1])                          # 99:新路徑
print(root_v1.children[0].children[0].children[1])                          # 2:舊版不受影響

兩個版本並存(持久性)、修改只花 O(log n)、未動過的子樹共享記憶體——不可變的代價被數學壓到近於零,這是 copy-on-write 快取友善與無鎖平行的基礎。

線索三:ownership 是線性型別的工程化#

Rust 的 ownership + borrowing 是「線性型別」lambda calculus 的變體:每個值恰好有一個 owner(線性使用),借用(borrow)分為可變(&mut,獨占)與不可變(&,共享)兩種,且互斥。這正是 Girard 線性邏輯的工程判決:

\[ \text{每個值} \; x : A \; \text{恰好被使用一次} \]

資料競爭(data race)在編譯期就被型別系統拒絕:兩個執行緒不能同時持有同一資料的可變借用。「共享可變狀態」這個敵人,被線性型別從語言層面消滅——不需要執行期鎖,也不需要垃圾回收。

線索四:async/await 是 continuation 的語法糖#

回呼地獄的解藥 async/await(JavaScript 2017、Rust 2019、Python 2015),其數學本源是 continuation passing style(CPS):每個 await 點把剩下的計算打包成 continuation,交給事件迴圈在未來恢復。

\[ \text{await } x \; \equiv \; x \, \texttt{.then}(\lambda r. \; \text{rest}(r)) \]

巢狀回呼被壓平成線性控制流,錯誤處理回歸 ?/try/catch 的代數。控制流不見了嗎?不——它被 monad(Future/Promise)與 continuation 收進型別裡。

線索五:GPU 上的純函數式——Futhark#

Futhark(2017,DIKU)把純函數式程式編譯到 GPU。GPU 有數千核心,共享可變狀態的鎖在 GPU 上根本不可行;但純函數無副作用,平行化是自動的:

\[ \text{map}(f, [x_1, \ldots, x_n]) = [f(x_1), \ldots, f(x_n)] \quad \text{每個 } f(x_i) \text{ 獨立,可同時執行} \]

純度決定了可平行性:無副作用 ⇒ 無資料競爭 ⇒ 自由平行。這是函數式思想最純粹的兌現場合。

結案 -- 後果與影響#

  • λ-Calculus 誕生九十年後(1936→2026),成為三大前沿的底層語法:
  • 機器學習:Google 的 JAX 用函數式轉換 jit/vmap/grad——只有純函數才能自動微分與安全編譯,PyTorch 的 functorch 亦同源。
  • 區塊鏈:Cardano 的 Plutus 用 Haskell、Tezos 的 Michelson 源自 OCaml——智能合約因為資產不可逆,必須形式化驗證,純函數語言是唯一可信的載體。
  • 平行運算:Futhark 到 GPU、Rust 在 Linux 核心與 Windows 的採用,證明無鎖平行的工業價值。
  • 函數式思想全面內建:Rust/Swift/TypeScript/Kotlin/Java Stream/C++20 ranges/Scala 3——「先學 Haskell 再用主流語言」變成工程師的實際建議。
  • Nubank(2021 年收購 Clojure 母公司 Cognitect)以 Clojure 支撐數千萬客戶的銀行系統,函數式語言正式進入一線金融。
  • 「以函數為萬物本源」的百年運動,自 Church 1936 年的 λ-Calculus、Backus 1977 年的圖靈獎演說、Hudak 1989 年的 Haskell 報告,到 2020 年代的全面內建,以全面勝利收案。

關鍵人物與文獻(條列)#

  • Alonzo Church——λ-Calculus 原作者;Alonzo Church, "An unsolvable problem of elementary number theory", American Journal of Mathematics 58, 1936.
  • John Backus, "Can Programming Be Liberated from the von Neumann Style?", ACM Turing Award Lecture, Communications of the ACM 21(8), 1978.
  • Chris Okasaki, "Purely Functional Data Structures", Cambridge University Press, 1998——持久化資料結構與結構共享的理論基石。
  • Jean-Yves Girard, "Linear Logic", Theoretical Computer Science 50, 1987——Rust ownership 的型別理論源頭。
  • Graydon Hoare 與 Rust 團隊, "The Rust Programming Language", 2015, https://doc.rust-lang.org/book;Ralf Jung et al., "RustBelt: Securing Foundations for the Rust Programming Language", POPL 2017.
  • Troels Henriksen et al., "Futhark: Purely Functional GPU-Programming with Nested Parallelism and In-Place Array Updates", ICL 2017.
  • Evan Czaplicki 與 Clojure/Rich Hickey 譜系:Rich Hickey, "Clojure", 2007, https://clojure.org;Nubank 收購 Cognitect, 2021.
  • Jean-Marc Eber 與 Tezos 譜系:OCaml 於 INRIA(Xavier Leroy et al., "The OCaml System", 1996–);Tezos 的 Michelson 合約語言, https://tezos.com.
  • Simon Marlow et al., "Haskell 2010 Language Report", 2010;Cardano 的 Plutus, 2021, https://plutus.cardano.intersectmbo.org.