09.3 為什麼無限資料流可以運作:共歸納法的數學基礎#
前言:一個看似自毀的定義#
在 Haskell 中,這行程式完全合法而且有用:
ones :: [Integer]
ones = 1 : ones -- 自己定義自己!
-- take 3 ones = [1, 1, 1] ✓ 正常運作
把 ones 代入自身,任何數學家的第一反應是「這是循環定義, 沒有良基(well-founded)基礎」。但在惰性求值下它運作良好, 甚至可以寫出整個費波那契數列:
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
-- take 8 fibs = [0,1,1,2,3,5,8,13]
為什麼循環定義沒有崩潰?答案是:我們從來不需要「造完」 無限資料,只需要保證「任何有限觀察都有答案」。把這句直覺 變成嚴格數學的理論,就是共歸納(coinduction)與 bisimulation(互模擬)。本章從餘代數語義出發,建立無限 資料流的數學基礎。
1. 歸納與共歸納:對偶的兩種證明觀#
回顧 09.2 的對偶表,並聚焦在「相等」的定義上:
| 歸納(induction) | 共歸納(coinduction) | |
|---|---|---|
| 物件 | 初始代數 \(\mu F\) | 終餘代數 \(\nu F\) |
| 定義方式 | 由建構子有限生成 | 由觀察者行為刻畫 |
| 證明工具 | 歸納法:對建構步驟歸納 | Bisimulation:構造不變式 |
| 「相等」 | 語法樹逐層相同 | 所有有限觀察結果相同 |
| 否定情況 | 不存在「無窮元素」 | 不區分「永遠觀察不到的差異」 |
關鍵區分在最後兩行。對有限列表,「相等」就是逐元素相同, 歸納證明乾淨俐落。但對無限串流,逐層比較永遠做不完—— 我們需要一種不需要「看完」就能證明相等的工具。
2. 餘代數語義:系統就是「觀察+演進」#
定義 2.1(餘代數系統)。一個 \(F\) -餘代數 \((S, \gamma)\) , \(\gamma: S \to F(S)\) ,把每個狀態 \(s \in S\) 映射到它的 「一層觀察與後續狀態」。
以串流為例, \(F(X) = a \times X\) ,餘代數是:
直覺:串流不是「資料」而是狀態機——每個狀態提供 一個當前值(觀察)與一個下一狀態(演進)。兩個串流相等, 不是「內容相同」(它們沒有完整內容),而是:
定義 2.2(bisimulation,互模擬)。 \(S\) 上的關係 \(R\) 是 bisimulation,若對所有 \((s, t) \in R\) :
- \(\pi_1(\gamma(s)) = \pi_1(\gamma(t))\) (當前觀察相同);
- \((\mathrm{next}(s), \mathrm{next}(t)) \in R\) (後繼仍在 \(R\) 中)。
兩狀態 bisimilar(記 \(s \sim t\) )若存在某 bisimulation 包含 \((s, t)\) 。共歸納原理: \(\sim\) 是最大 bisimulation,並且通常取它作為 \(\nu F\) 上的相等。
2.1 用 bisimulation 證明 ones 相等#
zeros :: [Integer]
zeros = 0 : zeros
證明 zeros = 0 : zeros(此處「=」取 bisimilar)。構造關係:
驗證:
- 觀察:head(zeros) \(= 0 =\) head(0 : zeros) ✓
- 演進:tail(zeros) \(=\) zeros,tail(0 : zeros) \(=\) zeros, 而 \((\text{zeros}, \text{zeros})\) 的規範形式 \((\text{zeros},\ 0:\text{zeros}) \in R\) ✓
兩條件滿足, \(R\) 是 bisimulation,故兩串流 bisimilar。∎
注意方法的特點:我們沒有做任何歸納,沒有「遞迴到基例」, 只是構造一個封閉的關係(不變式)並驗證封閉性。這是共歸納 證明的標準形態——與其說「證明相等」,不如說「證明差異永遠 觀察不到」。
3. 最大不動點:共歸納的序理論面貌#
bisimulation 的最大性有經典的序理論解釋。在關係的全體 \(\mathcal{P}(S \times S)\) 上(以包含為偏序),定義:
- Bisimulation 就是 \(\Phi\) 的後不動點: \(R \subseteq \Phi(R)\) 。
- ** \(\sim\) (bisimilar)是 \(\Phi\) 的最大不動點**,由 Knaster–Tarski 定理保證存在: \(\sim = \bigcup \{ R \mid R \subseteq \Phi(R) \}\) 。
與 09.2 的對偶完全一致:歸納等於取最小不動點(由下往上堆), 共歸納等於取最大不動點(由上往下削)。共歸納的「由上往下」 有個漂亮的解讀:從「所有串流都相等」開始,不斷刪去觀察 不一致的對,剩下的極限就是相等關係。這就是為什麼共歸納 證明只需展示「某個封閉關係」——它證明該對在刪除過程中 永遠存活。
graph TD A["全關係 S × S"] -->|"Φ:刪去觀察不一致的對"| B["Φ(S×S)"] B -->|"反覆套用 Φ"| C["Φ²(S×S)"] C --> D["……"] D --> E["最大不動點 νΦ = ~(bisimilar)"]
4. 範例:證明 fibs 的代數性質#
命題。定義 fibs = 0 : 1 : zipWith (+) fibs (tail fibs), 則 tail fibs 與 zipWith (+) (tail fibs) fibs bisimilar (即費波那契的移位關係)。
設 \(f = \text{fibs}\) , \(g = \text{zipWith (+)} (\text{tail } f) f\) 。 構造關係:
逐點驗證封閉性:
- 觀察層:head \(f = 0\) ,head \(g = \text{tail } f\) 的頭 \(+\) \(f\) 的頭 \(= 1 + 0 = 1\) ;而 \(0:1:\dots\) 與 \(g\) 的開頭吻合。
- 演進層:tails 之間的關係由 zipWith 的加法封閉性維持—— 每對的下一狀態仍是「費波那契式互補」的兩個串流。
有限視角驗證(take 5):
take 5 fibs -- [0,1,1,2,3]
take 5 (zipWith (+) (tail fibs) fibs) -- [1,1,2,3,5]
-- 正是 fibs 平移一位——移位關係成立
完整證明需構造無限關係並驗證封閉,模式與 2.1 相同;這裡展示 的是骨架:共歸納證明=構造不變式+驗證封閉。
5. 惰性求值:為什麼工程上真的能跑#
共歸納說明無限資料語義上有定義;惰性求值說明它實作上 能執行。兩者的接合:
- thunk(延遲計算):Haskell 中每個值在需要前只是承諾。
ones = 1 : ones編譯成一個 thunks 的循環圖: 第一個節點含1與指向自身的指標。 - 按需生產:
take 3 ones只強制三個 head/tail 步驟, 對應餘代數的三次 \(\gamma\) 觀察。執行過程就是有限次觀察, 而 bisimulation 保證每次觀察都有定義。 - 生產空間有限:循環圖只佔常數空間(一個節點), 不會無限膨脹——這是
ones不會吃光記憶體的原因。
-- 常數空間的無限串流:
ones = 1 : ones
-- 有狀態的展開(anamorphism 與共歸納的合流):
iterate :: (a -> a) -> a -> [a]
iterate f x = x : iterate f (f x)
nats = iterate (+1) 0
-- take 4 nats = [0,1,2,3]
風險面(09.2 已警告):foldr (+) 0 ones 對無限串流做 歸納式消耗,永不終止。安全守則重申:無限資料配「有限觀察 操作」(take、zip、scanl),歸納式摺疊只留給有限資料。
6. 前因後果:共歸納理論的源流#
timeline
title 共歸納與餘代數簡史
1960s : Park:不動點理論與自動機等價
1980 : Milner:過程代數 CCS,bisimulation 成為並行程式等價的標準
1980s 末 : Aczel:非良基集合論,以共歸納刻畫無窮自我指涉
1989-93 : Rutten / Turi:Universal Coalgebra 理論成型,
與 Aczel 的終餘代數語義合流
1990s : Haskell 的惰性實踐需要理論:Turner、Bird、
Meertens 的 Squiggol 學派整理 fold/unfold 代數
2000s : Coq/Agda 內建 CoInductive;反應式程式
(FRP)以行為=餘代數為語義最終的匯流點:並行系統(Milner)與函數式程式(Squiggol) 在餘代數會師。今天,Rx/ReactiveX 的事件流、FRP 的 Behavior 、協程、甚至區塊鏈共識的「等價狀態」分析,語義骨架都是 bisimulation。一個為驗證通訊協定發明的概念,成為一切無限 資料流的相等標準。
7. 章末小結#
- 循環定義(
ones = 1 : ones)合法,因為無限資料的語義是 餘代數狀態機:「有限觀察皆有答案」而非「完整建構完成」。 - Bisimulation:觀察相同且後繼繼續相似;bisimilar 是最大 bisimulation 關係,由 Knaster–Tarski 的最大不動點保證。
- 共歸納證明=構造封閉的不變式,不需基例、不需遞迴終止—— 與歸納(最小不動點、由下往上)完全對偶。
- 惰性求值的 thunk 循環圖實現了共歸納語義;執行即有限次 \(\gamma\) 觀察,常數空間。
- 歷史主軸:Park → Milner 的 bisimulation → Aczel/Rutten 的 終餘代數 → Squiggol 的 fold/unfold → 現代反應式程式。
下一步#
至此我們完成了第 07–09 章的弧線:從物件與態射的拼裝邏輯 (07.1),經函子(07.3)、Monad 的誕生與代數(08.1–08.3), 到資料構造的兩面——積與餘積、歸納與共歸納(09.1–09.3)。 回顧全程,你應該能看見一條統一的線:萬有性質與不動點 是所有構造的發動機。下一步的建議學習路徑:
- 伴隨函子:把 07.2 的 currying 與 08.2 的「monad 出自 伴隨」系統化,是通往更高層抽象(Kan 擴張)的門票。
- Monoid / Monad 的程式實踐:回到 08 章的 Reader/State/ Cont,練習 monad transformer 疊加(MTL)。
- 共歸納的程式專案:用
unfoldr實作一個事件流處理器 或簡單 FRP,體驗「行為=餘代數」的工程味。
理論的終點不是考試,而是下一次你寫 data、foldr、 unfoldr、do 時,能看見背後那張由 Eilenberg、Mac Lane、 Lambek、Moggi、Wadler 一路鋪來的地圖。