《Functional Design》Podcast 準備稿:遞迴、惰性與狀態
書名: Functional Design(Principles, Patterns, and Practices) 作者: Robert C. Martin(Uncle Bob) 系列: Reading Uncle Bob(恩普拉氏) 公開標題建議: Reading Uncle Bob|Functional Design.當外部世界逼你改狀態 (2/10) 涵蓋範圍: 第3章 遞迴與迭代 + 第4章 惰性求值 + 第5章 狀態管理
背景速覽
上一集建立了「沒有變數、用遞迴推進狀態」的紀律。這一集把函式式基礎這部分收尾,補上三塊讓 FP 真正能在現實世界運作的拼圖:遞迴的兩種風貌與效能取捨、惰性求值這個「無限串列」魔法,以及最關鍵的——當 I/O、GUI、多執行緒這些「外部世界」逼著你改變狀態時,FP 怎麼把傷害降到最低。第一部刻意的編排是「禁慾 → 放鬆」:先建立純粹的紀律,再一步步引進現實的妥協。
一句話重點
純函式式是個美好的修道院,但真實系統一定有 I/O、有時間、有並行——FP 的高明之處不在於假裝這些不存在,而在於把不可避免的可變性,嚴格隔離在一個受控的小角落(尾遞迴位置、STM、atom),讓系統絕大部分仍維持不可變的安全。
值得討論的重點
1. 遞迴有兩種,效能天差地遠
「迭代式遞迴」(尾呼叫,可被 TCO 轉成跳躍、堆疊不長)和「真遞迴」(呼叫不在末端、堆疊會長)是兩回事。Uncle Bob 用 Fibonacci 演示:照數學定義寫的 fib(n)=fib(n-1)+fib(n-2) 優雅到極點,但效能是 O(n³) 等級的災難——算 fib 34 要 735 毫秒。改成迭代版瞬間降到 0.15 毫秒,但失去了表達力。第三條路是 memoize:因為引用透明(同輸入永遠同輸出),算過的就記下來不再重算,於是同時拿到優雅與效能。這是 FP 獨有的甜頭——在會變的世界裡,你不敢這樣快取。
2. 惰性求值:把「做什麼」和「做多少」解耦
(range) 不傳參數會回傳一個「無界」的整數串列——它不是無限,是「你要幾個就給幾個」。關鍵洞見是 Uncle Bob 那句:惰性把「要做什麼」與「要做多少」解耦。你可以寫一個產生費氏數列的程式,完全不知道使用者要幾個;使用者自己用 take 500 決定。對照「(range 51) 把 51 綁死在函式裡」vs「(take 51 (range)) 把 51 外置」,這就是解耦的力量。代價是惰性會累積一長串延遲計算的迭代器鏈,失控會把記憶體吃爆——所以要適時用 doall 強制求值。
3. 「沒有複雜度高到必須放棄不可變性」
第 5 章的金句。Uncle Bob 拿他自己用 Clojure 寫的太空遊戲 spacewar 當證據:30 fps、企業號、克林貢、魚雷、爆炸全在跑,整個遊戲狀態裝在一個叫 world 的不可變物件裡,每一幀都是「用舊 world 算出新 world」,從不就地修改。複雜不是放棄不可變性的理由,問題只是「你願不願意用 FP 的紀律去組織它」。但真正逼你妥協的是外部框架(Swing 那種可變物件框架)和多執行緒——這時 FP 給你 STM(軟體交易記憶體)這個救生圈。
注意事項
⚠️ STM 和 atom 是這集最技術的部分,別陷進 compare-and-swap 的機制細節。聽眾只要懂一個對比:傳統做法是「上鎖」,但兩個鎖一遇上就可能死鎖(Uncle Bob 叫它「致命擁抱 deadly embrace」);STM 的解法是「不鎖,誰撞車誰自動從頭重試」。這個「樂觀並行」的直覺,比任何程式碼都重要。
⚠️ 「惰性」在 Clojure 裡是雙面刃,這點要平衡呈現。它強大(無限串列、自動釋放歷史值),但 Uncle Bob 自己承認寫過「成千上萬個惰性串列疊在一起、最後把記憶體撐爆」的程式。不要把惰性講成純粹的優點。
⚠️ 這集的三個主題(遞迴/惰性/狀態)份量厚、概念跳得快,是整季最「硬」的一集之一。錄製時節奏要放慢,每講完一塊用一句白話總結再進下一塊,免得聽眾掉隊。
專家補充
💡 memoize 那段藏著一個 FP 的深層美感:只有在引用透明的世界裡,快取才是「免費的正確」。在一個變數會變的系統裡,你快取一個函式結果,永遠要擔心「底層狀態變了、快取過期了」;但純函式的結果永遠不會過期。這就是為什麼 FP 社群對快取、平行化、記憶化這麼自在——純度直接買到了這些能力。
💡 「函式式核心 + 命令式外殼」(Functional Core, Imperative Shell)是這集沒明說、但貫穿全書的架構模式。spacewar 的 world 是純函式核心,Quil/Swing 是被隔離的可變外殼。這個模式後來被 Gary Bernhardt 命名推廣,也是《Grokking Simplicity》整本書的主旨(actions / calculations / data 三分法)。可以接過去,顯得這不是 Uncle Bob 一個人的怪招,而是一整個流派的共識。
💡 死鎖的「致命擁抱」和 STM 的對比,其實是資料庫世界早就解過的老問題——樂觀鎖 vs 悲觀鎖。Clojure 等於把資料庫的交易語意搬進了記憶體。懂後端的聽眾聽到「compare-and-swap、衝突就重試」會立刻會心一笑,因為那就是樂觀鎖。
討論問題
🎙️ 開場鉤子:「上集我們說 FP 沒有變數、沒有競態條件——聽起來太美好了。今天就來看,當真實世界的 I/O、GUI、多執行緒撞進來,這個修道院還守得住嗎?」
🎙️ 自問自答:「既然真遞迴效能這麼爛,為什麼還要學它?」——因為它最接近數學定義、最好讀;而 memoize 讓你不必在『優雅』和『快』之間二選一。這正是 FP 想給你的:先寫對、寫漂亮,再讓引用透明幫你變快。
🎙️ 帶走的一題:「下次寫多執行緒程式時,先問自己一句:這裡真的需要『變數會變』嗎?還是我可以把要變的狀態,集中隔離到一個 atom、一個 STM 交易裡——讓其餘 99% 的程式保持不可變?」
更大範圍關聯
- 接續本書第一部:這集收尾「函式式基礎」五章,下一集起進入第二部「比較分析」——同一題用 OO 和 FP 各寫一遍,差異開始拉開。
- 與《Grokking Simplicity》對話:Eric Normand 那本把「actions / calculations / data」講得最清楚,正是這集「隔離可變性」的系統化版本,是給聽眾的絕佳延伸讀物。
- 惰性的兩個極端:Haskell 內建全面惰性、Clojure 是「函式庫多半惰性、避不開」、F#/Scala 要明示。這條光譜本身就是一堂 FP 語言設計史。
- STM 的歷史地位:Clojure 是把 STM 推進主流的關鍵語言之一;這個概念源自 1990 年代學術界(Shavit & Touitou),Rich Hickey 讓它變成日常工具。用 [軟體工程領域切分藍圖] 看,這扣到「並行與分散式」那一支。
錄製建議
- 建議時長:約 24 分鐘(本集份量大)。配比:遞迴三版本+memoize 約 8 分、惰性與解耦約 7 分、狀態管理與 STM 約 9 分。
- 討論策略:遞迴那段用「735 毫秒 vs 0.15 毫秒」的數字對比製造戲劇張力,再用 memoize 收尾,聽眾會有「原來可以兩全」的爽感。惰性別講太深,抓住「做什麼 vs 做多少解耦」一個金句即可。狀態管理是重頭戲,用「沒有複雜度高到必須放棄不可變性」當論點、spacewar 當證據、STM 當救生圈,三段式講完。
- 這集偏硬,記得多用生活化比喻(西斯永遠是兩個、致命擁抱、快取像鑽石永遠都在),讓厚重的概念透氣。