一句話的函數式定義:不使用賦值述句的程式設計。值一旦初始化就不再改變——狀態的演進靠「從舊狀態算出新狀態」,而不是覆寫舊的。沒有賦值,就沒有時序耦合、沒有競態條件。這是許多設計模式在函數式世界裡「縮成一行」的底層原因。
🧠 When to Use
TIP
訊號:你的 bug 最後是靠「交換兩行的先後順序」解決的、或是加一行 log 卻發現有三個位置可放、其中一個印出錯的值——這些都是賦值帶來的時序耦合在作祟。
- 邏輯無共享狀態、想要好測試與好並行 → 用不可變值消除隱藏依賴。
- 資料「進來→變換→出去」的管線(解析、轉檔、聚合)→ 每步產生新值、不改舊值。
- 需要保留歷史(undo、稽核、快照)→ 舊狀態天然被保留,見 Memento。
⚖️ Structure & Variants
賦值 vs 初始化 —— 差別是『值會不會變』
- 初始化:
val x = 0——x此刻誕生並被設為 0;我們無從得知它是變數還是常數。 - 賦值:
x = 1——x早已存在,現在被改成 1;毫無疑問它在變動。 - 函數式的另一種說法就是**「沒有變數的編程」**:所有被命名的值都是常數,一旦初始化,其歷史完整、不變、永久存在——這就是不可變性。
用遞迴取代賦值 —— 狀態靠參數傳遞
// 命令式:靠賦值累加(sum、i 一直在變)
fun sumSquares(): Int {
var sum = 0
for (i in 1..10) sum += i * i
return sum
}
// 函數式:沒有賦值,新值靠參數傳給下一次呼叫
tailrec fun sumSquares(sum: Int = 0, i: Int = 1): Int =
if (i > 10) sum else sumSquares(sum + i * i, i + 1)- 想擺脫賦值,就得用遞迴:遞迴用「函式參數初始化」取代了「區域變數的賦值」。
tailrec(尾呼叫優化 TCO)讓遞迴不吃堆疊——所有函數式語言都以某種方式提供它。
引用透明性 —— 呼叫可用回傳值替換
- 數學函數
y = f(x):給定x永遠得到同一個y,系統當下狀態與先後順序都與它無關。 - 這性質叫引用透明性:任何函數呼叫都能安全地用它的回傳值替換。這正是 functional 一詞的由來。
- 純函數沒有隱藏依賴,因此好測試、好快取(memoize)、好並行。
不改狀態,怎麼寫『有狀態』的程式?
// 從舊狀態算出新狀態,而非修改舊狀態
tailrec fun run(s: State): State =
if (s.isFinal) s else run(s.advance())- 每次迭代產生全新的狀態,舊狀態從未被覆寫(精神上它們都還留在某個堆疊框裡)。
- 嚴格說,任何接受輸入的程式都不可能是純函數式(輸入不是引用透明的);但整體仍可用函數式風格寫成——這才是重點。
⚠️ Misuse & Anti-patterns
- 時序耦合:
open/close、new/delete、malloc/free這類「永遠成對」的操作,強迫程式碼照順序執行;忘了配對就出事。不可變性從根上消除它。 - 拿垃圾回收與鎖當拐杖:GC 是「我們承認不擅長處理時序耦合」而引進的工具;競態條件則是共享可變狀態下順序偶爾出錯的後果。減少可變狀態,才是治本。
- 假不可變:把物件宣告成
val卻裝著可變集合/可變欄位,內部照樣被改——不可變要一路到底(deep),否則只是心理安慰。 - 教條化:對效能關鍵的熱路徑,適度的區域可變是合理取捨;不可變是預設,不是宗教。
🔑 Takeaways
- 函數式 ≈ 沒有賦值 / 沒有變數:值不變,狀態靠「舊→新」計算推進。
- 想去掉賦值就用遞迴 + TCO;純函數具引用透明性,好測、好快取、好並行。
- 沒有賦值就沒有時序耦合與競態條件——這是函數式最實際的紅利。
- 不可變是許多模式在 FP 裡簡化的前提,也讓 Prototype、Flyweight、Memento 這類「複製 / 共享 / 快照」模式更安全。
No notes yet — jot your takeaways or Q&A here.