永恆不變的魅力
一個程序員,無論你人生的第一個hello world是從basic開始,c開始,抑或javascript開始,接下來了解的一個概念一定是「變數」(variable)。變數是可變的(mutable)。數學中永遠不可能成立的 x = x + 1 在編程語言中有了新的內涵:賦值。一個變數的生命周期里,只要需要,其值隨時改變。這改變可以是因迭代而發生,或者因狀態變化而發生。在這個概念的基礎上,程序員寫下的代碼,基本上就是根據外部或者內部的各種事件,對內部的狀態不斷進行改變。運行中的進程如此,磁碟的文件系統如此,資料庫如此,javascript控制下的DOM頁面也是如此。「變數」帶來了可變的狀態,而這種mutation在計算機的世界裡無處不在。
一個東西可變,這是件讓人很頭疼的事情。除非小心翼翼地把狀態遷移中遇到的所有動作都replay一遍, 你無法很容易將其恢復到前面的狀態(undo),或者重做某個步驟(redo)。在多任務的環境下,讀者寫者(或曰生產者消費者)之間的競爭和同步是個災難。就這樣一個簡單的場景,學術界和工業界還要將其分解成single producer single consumer,single producer multi consumer和multi producer multi consumer等多種場景,再加上lock-free和wait-free的各種組合,已經複雜到了一定範疇。
複雜是軟體的大敵。當一件事情複雜的時候,就意味著圍繞著它,會產生更多複雜的事情。我們想想測試:對單個函數或者類的單元測試一般都很簡單,因為它涵蓋的狀態最少;涉及到多個類交互的功能測試複雜一些,因為狀態開始交織纏繞。做網路設備的同學大抵都聽過一種叫stress test的測試方法,就是把設備放在一個複雜的網路流量環境下,開啟各種功能後,跑72小時。對此,工程師們都會祈禱千萬別出任何問題:因為在這樣一個無數狀態組織成義大利麵條的複雜場景下,出了問題,找到根源的機會很小。
既然,可變的狀態如此難以捉摸,乾脆,不允許 x = x + 1,讓任何狀態都不可變呢?
想想都嚇人,這會引入多少問題!難道我每次在數組或者哈希表裡修改一個元素,就把所有已有元素全部重新複製一遍?GC造的過來么?我還怎麼寫for循環,甚至,怎麼愉快地寫代碼?
別急,這些問題先放一放,我們看真實世界裡幾個產品例子。
先說git(或者任何scm)。每當你往版本庫裡面提交一個commit後,你便失去了再次更改它的權利 [1],任何之後的修改都不會改動之前的commit。系統通過計算兩次commit間的diff,以增量的方式保存數據。有了這樣一個基礎,你就可以隨意undo / redo,把自己的代碼庫切換到歷史上任何一個commit。而且這個commit永遠恆定。多人協作別人也無法破壞你的commit。
docker從git身上學到了很多思想,它把本不是什麼新鮮事的lxc包裝成了一個類似於git的部署管理軟體。軟體部署一直是件頭疼的事情,由於文件系統是可寫的,想要重構一個運行時的系統,唯有把其經歷的所有步驟replay一遍:這是之前部署管理軟體,如puppet,ansible所做的事情。replay是件費時的事情,是對初始狀態不斷修改,最終達到需要的狀態,典型的處理mutation的思維。
而docker另闢蹊蹺:如果運行環境也和git的一個commit一樣,每次環境的改動提交後,都是immutable的呢?
這個大膽的想法成就了docker今日的輝煌。git兩個版本間的diff多為文本,而docker則是文件系統的diff。使用已有的UnionFS(也許現在換了新的FS?),通過copy-on-write不斷產生新的layer,新layer只保留和已有layer不同的地方,軟體的運行環境成了一次次commit(一個個layer)。再輔以強大的cache能力,docker實現了 "git for deployment" —— 管理員可以在生產環境很方便地undo/redo/checkin/checkout出來各種版本的運行時。
react則是immutability在UI領域的一次偉大的嘗試。當同期的javascript庫還沉迷在MVC的初級階段,爭做其頭牌,react已經把目光放得更遠:有沒有什麼思想上的飛躍,能夠做出來一個更加牛B的V(iew)?
react想到的是減少變化。它有兩個主要的思想:信息單向流動以及隔離變化,而隔離變化主要體現在兩方面:
1) 把不可變的props和可變的state分開 [2]。
2) 在DOM和DOM操作者間安插了VDOM [3]。與其直接操作dom,把狀態維護得亂七八糟,我何不做個中間層,所有新的修改都是一層層累加上去,可以獲取diff,就像git一樣?基於此,react不必費心記錄用戶究竟具體做了那些binding(像ember/angular那樣),而是每次改動,通過VDOM算出一個新的diff,對DOM做最必要的改動。
當然react不可免俗地還是使用了變化的state,這使得其計算diff的performance並未達到最好。而基於react的om [4],藉助cloujurescript [5] 的語言層面的immutability,把react的能力發揮到極致(state的變化本身就以diff記錄,所以效率超高)。當UI能夠用簡單的數據結構EDN [6] 來表述的時候,一切都顯得那麼簡單。想在UI上來個時間旅行(undo/redo)?這就跟git checkout一個commit那麼簡單!想對UI的任意一個狀態做測試?太簡單不過 —— 事先構造好一個中間狀態的UI的EDN,然後進行特定的步驟,測試構造好的EDN是否於實際UI的EDN相等不就得了!
下面這個pixel editor只用了66行代碼 [7],就提供了一個完整的undo/redo的功能:
這就是immutability,永恆不變的魅力!
說到這裡,我們再來回答immutability帶來的問題。怎麼愉快地寫代碼的問題,已經由函數式編程語言解決,程序員只需付出時間和精力去適應這個陌生的世界即可。
剩下的問題是,怎麼愉快地複製狀態?
數學家們早就提供了解決方案,clojure將其引入到實際應用:persistent vector。下圖的數組使用了persistent vector,4個bit 4個bit(實際使用是32bit)建成一個索引樹,假設我們要改索引是106(0b01101010)的元素,首先取(01),也就是1,找到第二層第二個索引,以此類推:
最終到達葉子結點,找到要修改的元素後,創建新的節點,並將這條鏈上的走過的所有索引一併複製,就完成了數組內元素的一次「修改」。
有同學肯定想O(1)的問題愣是搞成了O(logN),這不脫褲子放屁多此一舉么?
別忘了這裡不僅僅是「改變」內容那麼簡單,還記錄了歷史,保有了數據進行時間旅行的權利。而最美妙的是,犧牲一些運行速度和內存,你的代碼是immutable的,是化繁為簡的。
而immutable,是如今這個程序世界夢寐以求的。我知道現在大家講到immutability的好處,就必然會講並發,所以並發的好處就不講了。
看了之後還想了解更多?看這個video:https://www.youtube.com/watch?v=mS264h8KGwk。我想你應該會科學上網的。
如果您覺得這篇文章不錯,請點贊。多謝!
歡迎訂閱公眾號『程序人生』(搜索微信號 programmer_life)。每篇文章都力求原汁原味,北京時間中午12點左右,美西時間下午8點左右與您相會。
1. git commit --amend可以算一個小小的,無傷大雅的妥協2. props和state的分別以uber的這篇文章最佳:https://github.com/uberVU/react-guide/blob/master/props-vs-state.md3. 又是indrection的思想,還記得我寫的indrection的文章么?4. 見:https://github.com/omcljs/om5. 見:https://github.com/clojure/clojurescript6. om使用EDN,見 https://github.com/edn-format/edn7. 自己看代碼:https://github.com/jackschaedler/goya/blob/master/src/cljs/goya/timemachine.cljs推薦閱讀:
※ps4和swich選擇哪個比較好?
※拳皇14. 鐵拳7.街霸終極版,這三個PS4遊戲只能選一個的話如何抉擇?
※全面對比 Xbox One 和 PS4,入手哪個更好?
TAG:迷思 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |