在程式中時間旅行
Nor Time, nor Place, nor Chance, nor Death can bow. My least desires unto the least remove. - “The Time Traveler’s Wife”
三年前,當我寫下那篇青澀的『永恆不變的魅力』的文章時,我剛剛是 elixir 和 clojure 的入門者。我如飢似渴地從 Bret Victor,Rich Hickey 等人身上吸取思想和力量,來澆築我對函數語言程式設計的信仰。函數語言程式設計語言中有諸多讓人讚不絕口的設計思想,但 immutability(不可變)顯然是皇冠上的明珠。它讓我們可以肆無忌憚地使用併發,不必考慮 lock,因為沒有 critical section 可言;它讓我們不必再終日在野指標造成的 segment fault,壞引用導致的 exception 中彷徨哀怨甚至自戕。當我們用一個產品的時候,確定性讓我們感到安全和愉悅 —— 你使用微信,發給朋友的資訊如果代表傳送的小菊花停了沒有驚歎號,那就一定成功了,這就是確定性;immutable 給程式員帶來的確定性是:我給你一個引用,只要你拿著,就算到了天荒地老,海枯石爛它也能夠訪問,且還守候著原來的值。
有沒有想過,這麼好的東西,為什麼前輩們不使用呢?
非不為也,實不能也。immutable 是好,但是「浪費」記憶體啊。
當機器的記憶體以 KB 為單位時,描述複雜狀態的程式都力不從心,自然只能儘可能地重複利用每一個 bit,以期它能發揮最大的作用。什麼 “copy on write”,一邊去。
八九十年代,記憶體雖然到了 MB 級別,但 DOS 受限於真實模式的定址能力,還是把記憶體分成了五環內(低 1M)和五環外(超過 1M 的 expanded memory),所以那時的程式設計師依舊以扣記憶體聞名。
千禧年後,記憶體在亞 GB 級別往 GB 級別過渡,作業系統早已完全採用 32 位保護模式,正往 64 位過渡。大部分應用程式開發者漸漸無需考慮記憶體的天花板,就算一不小心把實體記憶體用超了,還有 swap 兜底,管夠。但是,牛頓告訴我們,程式設計師是懶惰的,沒有外力作用的時候,會產生慣性,也就是路徑依賴。
路徑依賴導致 immutable 的思想人人叫好,但依舊只是用在少量的場合,比如 git,docker,react,當然,還有區塊鏈。
還有一個原因。看待世界的方式。我們所執行的世界究竟是一個可變系統(mutable system)還是一個不可變系統(immutable system)?我們先來研究自己的大腦。大多數人第一反應可能會覺得大腦是個可變系統,然而仔細想想,它是不可變的。我們的記憶就像洋蔥圈一樣,不斷疊加,而不會修改。拿我家的電話號碼來說,我腦袋裡能立刻蹦出來好幾個號碼,有四位數的,那是小時候家裡安裝的第一部電話,有廣東的,北京的,聖何塞的,西雅圖的,等等。所以大腦是個不可變系統,資料一旦產生,大腦只會將其連線起來,並不會修改。我們再看日常發生的各種事件 —— 今天的氣溫,世界各地的新聞,樓下彈鋼琴的孩子,所有這一切都是不可變的。氣溫看上去在變化,但這取決於建模的方式,如果氣溫不是一個值而是一個以時間為刻度的 vector 呢?
所以當我們把世界看成一個個只有最終狀態的點的時候,它是不斷變化的;然而加上時間的維度,它是不可變的 —— 在一個初值 (genesis state) 下,發生了一系列不可變的事件(event),最終導致了當下這個狀態(state)。這是我們這個世界運作的方式,可惜,在大部分時間,不是我們撰寫程式的方式 —— 即使我們的程式要麼和現實世界打交道,要麼在模擬現實世界。
Bret Victor 在他著名的講座 Inventing on principle,展示了改變認識如何讓我們擁有一個又一個 voila moment,比如這個截圖,遊戲中的時間旅行:

在程式中做時間旅行並不是件新鮮事,我們每天使用的 git 就可以讓我們自如地在歷史上發生的任何一個 commit / tag / branch 上切換:

而 clojure 的一個 pixel editor,Goya,也用時間旅行的方式來做 undo / redo,非常簡單:

而要做到這一切,首先我們要使用 genesis state + events 的方式來描述應用程式裡的世界。在 git 裡,一個個 commit 就是一個個 event;在 goya 中,畫筆的每一次動作,就是一個 event。
然後,我們需要用一個合適的資料結構來儲存 state - Chris Okasaki 的 Purely Functional Data Structures 向我們揭示了 persistent data structure 的神奇魔力 —— erlang 的發明者,Robert Verding 據說就是照著 PFDS 這本書的例子寫出了 erlang 的資料結構的支援:

在這裡,資料儲存在葉子節點上,然後以資料的索引為基礎構建出來一棵樹。當整個結構的某些資料變更時,我們只需要產生新的資料,然後產生索引該資料和未改變資料的一棵新的樹,從而在空間上避免 immutable 產生的拷貝。在函數語言程式設計語言中,舊的樹如果沒有人用了,GC 就回將其回收,但如果我們把每個 event 產生出來的新的樹和舊的樹連結起來,或者記錄下來,就具備了時間旅行的能力。
git 使用了 Merkle tree(更準確地說 Merkle DAG)來儲存所有 commit 的所有 object。和 persistent data structure 思路類似,資料(commit 裡的 objects)在葉子節點上,只不過連線葉子節點的索引是其 hash,而非普通的 key。Merkle DAG 的使用非常廣泛,從 Plan 9 OS 到 BitTorrent,從 git 到 bitcoin / ethereum,等:

我們知道,通過初始的 genesis state,不斷順序疊加 event,可以構成任意時刻的 state,這樣的設計思路是 event sourcing。而使用 event sourcing,把當前的 state 用 Merkle DAG 管理並儲存下來,再使用公鑰加密演算法使所有的 event 和 state 都 public verifiable,就構成了我們所熟知的以太坊。
在以太坊裡,其 event 是 transaction,state 使用 Merkle Patricia Tree 儲存。如果把以太坊看成是一個自給自足的世界,那麼其 fork 就是這個世界在平行宇宙中的另一個世界,而我們人類就像『星際穿越』中的五維人,可以在以太坊世界中進行時間旅行 —— 比如,我要回到 2017 年 1 月 13 日,去探索那個時間點這個世界裡所有賬戶的狀態,只需要找到 4904084 這個塊裡的 state root,找到這個 hash 下面對應的數,然後從這棵樹一路往下挖掘資訊。
那麼,如果我們需要以太坊任意一個時刻的狀態呢?回答這個問題之前,我們先來回答,在以太坊的世界內,時間究竟是什麼?時間是區塊的高度。所以,在以太坊內進行時間旅行,就是在獲取不同塊高下的 狀態 。然而,以太坊自己的時間對人類來說是晦澀的(1 eth second ~= 12s),所以我們需要先將人類社會的時間對映到以太坊上的塊高,然後找到給定的人類時間下最接近的塊高,就得到了以太坊下的任意時間內的時間旅行。
以上。
我下週一回國,會在上海蔘加 kubeconf;在深圳出席 techrunch 的黑客馬拉松,代表 arcblock 負責區塊鏈方向;以及在微軟做一次區塊鏈的深度技術講座,歡迎參加活動的讀者們勾搭。