Free mind

          Be fresh and eager every morning, and tired and satisfied every night.
          posts - 39, comments - 2, trackbacks - 0, articles - 0
             :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

          構造出健壯軟件

          Posted on 2007-04-24 09:51 morphis 閱讀(379) 評論(0)  編輯  收藏 所屬分類: 0. Daily feeling

          有時無知是福。俺看到一點新鮮的科普也能覺得造化神奇。剛才讀Gerald Jay Sussman(SICP作者)的文章,Building Robust Systems – an essay,竟然心如小鹿亂撞,手心濕潤,仿佛第一次握住初戀情人溫柔的手。

          這篇文章主旨明了:構造復雜的健壯系統非常困難。我們的軟件能夠有效完成某件具體任務,卻不能適應業務領域的變化。一點細微的需求或部署的改動都能讓我們的系統變得脆弱。反觀生物進化的歷史,無數生物在頻繁變更的嚴酷大自然里繁衍生息。也許我們能從中學到讓系統隨環境變更而自動演化的秘密。當然,不是所有的軟件都身段僵硬。35年前問世的Emacs至今是最高效的編輯器之一(不怕惹眾怒地說一句,流行的EditPlus和UltraEdit除了學習曲線,還是不能和Emacs比),實屬居家旅行殺人越貨之必備利器。20年前問世的TeX ,1989年就停止更新代碼,但這并不能阻擋它橫掃科技出版業,變成科技出版物排版的事實標準,基本上干掉了科技出版業里的排版員這項本來很有前途的工作。不過,這些都是特例。我們需要了解的是構造復雜系統的通用方法。不然skynet和matrix怎么能夠問世囁?

          Sussman在文章里討論了生物在殘酷進化里得來的五坨特性。這些特性對諳熟生物的老大們也許不是新鮮事,卻能讓我這個靠三思科普生物的半文盲腎上腺素急劇釋放:

          •  冗余(redundancy)和簡并(degenerate,不知道這個翻譯對不對)
            強健的生物系統都有大量冗余。比如腎和肝。割掉一個腎,我們仍然能活蹦亂跳。把肝的三分之一供奉給乙肝病毒,我們被歧視乙肝攜帶者的公司氣死的幾率也大多得肝癌掛掉的幾率。

            生物系統也高度簡并:同一功能可以由不同的部分來完成。比如我們的能量既能從糖獲得,也能從脂肪獲得,還能從蛋白質里獲得(這個造成我們為節食和消耗熱量絞盡腦汁,實屬進化跟不上變化的特例,另當別論)。而這三者的代謝過程都不一樣。甚至我們的遺傳代碼也是簡并的。我們有20來種氨基酸,但核苷酸構成的密碼子組合卻有64種。這樣才能讓點突變不至于影響某個密碼區的蛋白質,使得突變能積累起來,同時不會導致明顯的表現型后果。不然領導某天生出一特聰明的小孩兒(變異了),但就是長得像頭羊,我們非得抓狂不可。簡并成于進化,也成就進化。環境改變了,大不了某個部分廢掉,但生命繼續(系統依然滿足規格)。而廢掉的功能為變異(或修補)騰出空間。整個系統沒有被驚擾。

            現在的軟件系統往往包含冗余,但很少刻意加入簡并。作者順便對Python拍磚:Python的口號是TIOWWTDI(There is only one way to do it),明顯是零簡并系統。
          • 探索行為
            探索行為也是生物系統健壯的基石。生物需要的合適功能通過“生成-測試”的機制獲得。系統某部分生成功能,而另外相對獨立的部分測試功能,決定接受還是拒絕測試結果。比如說支持細胞上的微管陣列決定細胞的形狀。微管們總是不斷被生成和摧毀。那些有幸碰到細胞膜里穩定子的微管們得以存活。最終結果就是細胞的形狀又穩定子的位置決定。所以細胞形狀的生成和維護機制同確定細胞最終形狀的機制分開(操作系統設計里policy和mechanism分開有點類似)。

            探索過程中測試者不必知道行為提供者。行為提供者也不必知道測試機制。這樣的結果是變異和適應非常靈活,因為測試者和行為提供者可以自由發展。反正合者生,不合者死。負面作用就是這種選擇代價高昂。自然選擇的每一秒都伴隨著無數生命的消亡。
          • 隔離和定位
            我們身體的每一坨細胞都源于單一的受精卵。所有細胞的遺傳信息(才1GM內存!)都一樣。但是,我們有各種專門細胞,比如皮膚細胞,神經細胞,肌肉細胞等。這些細胞再進一步組織成組織,器官,和器官系統。這一切之所以可能,是因為細胞們能根據環境特異化。也就是說,細胞的行為并沒有被編入細胞間的信號傳遞,而是由基因組決定。不同的信號組合可以激活或關閉細胞的某項功能。不同的細胞組合起來,進一步實現更為復雜的功能。

            優秀的軟件系統有類似的特點。它們高度模塊化。不同的模塊在不同的環境里執行對應的功能,進行不同的組合。

          • 防御,修補,和再生
            生物大都能防御異物的攻擊,修補缺損的部分,再生死亡的肌體。現在的軟件開始包括防御功能,比如安全特性。不過,很少有強大的修補和再生功能,雖然號稱自己能自我調控的系統不少。前兩年IBM鬧騰得歡的autonomous computing最近好像也讓位給牛皮轟轟的SOA。Erlang的容錯系統倒非常誘人:它的基礎設施讓系統比較輕松地偵測出問題的進程,然后在不影響系統運行的前提下重啟該進程。

          • 組合
            復雜系統都是又小模塊組合而成。這點我們并不陌生。我們構建的系統模塊依賴事先寫好的規范。比如接口的規范,比如接口調用的順序。這種規范現行的辦法隨著系統復雜程度的增高變得越來越難。相反,人類的基因組信息不過區區1G,還不夠容納一個普通操作系統的規范,卻足以決定我們的構造和行為。我們的“系統接口”必須能夠自我配置,適應環境。代價是系統的初始化時間太長,9月懷胎不過是剛開始。

          文章也討論了具備這些通用特性的健壯系統需要哪些基礎功能:

          • 通用的部件
            健壯的系統總是由一系列通用部件構成。每類部件都有廣泛的應用范圍。每個部件能接受范圍寬廣的輸入,但能輸出范圍狹窄的結果。這好比久經考驗的電子原件。他們能在充斥了噪音信號的環境里正確工作,輸出可以預測的信號。
          • 可以擴展的泛型操作符。比如同樣是加號,+, 既可以處理整數相加,也能處理實數相加,也能處理矩陣相見。更重要的是,還能讓用戶擴展該操作符的語義,引入新的功能。這樣不僅能讓新程序容易編寫,而且能舊的程序自然增加功能,應對新的環境。這點其實很多語言都有所支持,尤其是現下流行的動態語言。比如說在Lisp環境下開發,我們可以開發一個簡單的版本,讓程序運行起來。然后我們就在這個運行的程序里不斷調試,修改,和加入新的功能,直到這個系統健全。這里有演示錄像。
          • 生成和測試
            我們的系統應該讓我們能夠寫出返回多項選擇的函數,然后同步測試這些結果,挑選出合適的結果。如果結果不好,系統自動回溯。這樣做的危險是回溯可能導致指數級的運行時間。不過這也是進化不可避免的代價。其實生成-測試的理念早已用到編程當中。比如Prolog的自動回溯。和眾多動態語言支持的模式匹配--比如Erlang, 比如Scala,比如Ocaml。
          • 通過約束得到的普適過程
            這個大概是說通過一系列的約束條件,我們可以構建出對應的約束網絡。通過這個網絡,我們能生成適應這些約束條件的過程或函數。這樣生成的函數能滿足廣泛的應用環境。
          • 工程中的簡并
            作者提出了兩種方法。一是利用AI中常用的技巧:目標導向(goal-directed)的方法。這個本質上是說我們不規定系統怎么做一件事,而是告訴系統要達到什么目標。Prolog簡直是解決這類問題的天生殺手。當然,現代的函數編程語言也是解決這類問題的利器。二是對同一問題實現多種獨立解法,然后讓系統挑選合適的方法。

          文章還討論了支持系統健壯和進化的方法:

          • 組合子
            簡單說,我們應該通過搭配組合相對獨立的部件來構建系統。函數語言對這種編程方式尤其擅長。特別是支持transparent referential integrity和lazy evaluation的函數語言,比如Haskell:我們搭建出基本的模塊。Haskell系統提供一大堆強大的黏合工具,讓我們把這些模塊輕松地組裝起來。不過文章也強調,關鍵還是模塊的質量。函數編程只是有效的手段。
          • Continuation
            這個特性在Ruby, Python,Scheme, Smalltak等現代動態語言里都有。Continuation是非常強大的編程手段。一個Continuation對象被創建時能保留當時系統的有關狀態,并在其他任意時間被調用。這讓程序員獲得對時間的直接控制。我們可以暫停某段計算,并在一段時間后繼續那段暫停的計算。牛人Avi就利用Smalltak對Continuation的完善支持寫出了絕對讓人驚嘆的Seaside Web編程框架。不信邪的老大們可以去體驗以下用Seaside搭建的應用,dabbledb。
          • 回溯和并發
            沒有回溯,我們就不能自動檢驗多項選擇,排除不合適的結果。沒有并發,我們不能同時檢驗多個結果,計算的代價太高。這些好像不新鮮。
          • 任意聯系
            標注元數據便是一個例子。我們不能也不可能預測系統運行時數據間的所有關聯。所以構建可以任意擴展的數據關聯系統就非常重要了。這好像也不新鮮。比如說至少20年前人們就認識到Metaobject Protocol的重要性,有興趣的老大可以去讀這本經典的書?,F在Java支持的Annotation也可以算一個例子。
          • 動態配置的接口
            這個比較新鮮,屬于正在研究的難題?,F在的方法是讓一個群體內的計算實體(agent)通過不斷地交流信息來建立大家理解的規則。俺沒有看明白。

          總之,這篇文章屬于高來高去的主題演講性質的文章,但它對生物系統以及生物系統與計算系統的聯系的描述著實讓我大開眼界。也許文章結尾是最好的總結:正經的工程不過幾千年歷史。我們構造健壯系統的手段還遠未成熟。我們還沒有從漫漫幾十億年生物進化中吸取經驗….

          Sussman認為構造出健壯軟件需要我們的系統支持continuation, 回溯,和生成-測試的方法。生成-測試最直觀簡單的方式是為系統提供多項結果。系統一個一個地測試這些結果,并接受符合要求的一個。Sussman舉了一個例子:平方根函數通常返回正根,而拋棄那個負根。那按照生成-測試的方法,一個平方根函數應該將負根和正根一起返回,然后由系統決定到底哪個根更好。后來他進一步提到(第20頁最后一段)可以返回正負根的AMB值。昨天寫帖子寫得頭暈眼花,竟然忘記討論這坨精彩的函數(編程意義上)。今天補上。我們可以看到一個簡單(簡單不等于淺顯哈)的抽象,居然能實現眾多美妙的功能。一個看似無意創造的玩具,竟然是有助于揭示構建輝煌軟件大廈的秘密。
           
          AMB這個操作符歷史久遠。AI大牛,LISP的奠基人,John McCarthy在這篇1961年的論文里首次提出了模糊函數(ambiguous function)。我們一般把這個函數簡寫為AMB。似乎最近俺讀的文章也好,書也好,都越來越年代久遠。不過不怕打擊喜歡追新的老大們:現在流行和將要流行的技術背后的理念都至少有20年的歷史。比如這個AMB函數,雖然1961年就被提出,但知道1987年還有研究它的論文發表?;ヂ摼W?Google一下Douglas Engelbart。面向對象?Google一下Simula和Alan Kay。垃圾收集?Google一下LISP。BPEL?Google一下Robin Milner。作為編程語言的XML? Google一下S-Expression。元編程?Google一下Metaobject Protocol和CLOS。這其實符合技術革新的規律:大學里新奇的編程手段差不多要20年到30年才被大眾接受。新理念提出后,還得被無數學者驗證,潤色,完善;還需要無數工程師實現,優化,改進,推廣。所以說呢,如果老大們不滿足于趕潮而憧憬弄潮,慢下來,向后看,也許是不錯的手段。跑題老,還是說這個AMB操作符。俺最早在SICP上讀到關于AMB的討論。SICP也算是一本奇書。大一的入門教材,問世20來年了,里面的內容仍然讓人拍案叫絕。每年翻開這本書,都能學到一點新的東西。書的第三章已經開始詳細討論并發和stream。第四章詳細講述了夢幻般的meta-circularity,接著就是現在開始熱門的lazy evaluation,和logic programming。而第五章干脆教我們搭建一個簡單的寄存器機器。又跑題了。最近越來越像老人家,變得嘮叨。通俗講,amb接受0或多個參數,并不確定地返回其中一個參數。比如說,amb(1, 2)可能返回1,也可能返回2。如果amb沒有參數,那它不返回任何值,并拋出異常。也就是說,amb()一定掛掉。同時,amb的參數可以是一個函數(對用C/C++語言的老大來說,就是一個函數指針),amb返回的值必須是不會拋出異常的參數。比如說,amb(amb, 1)必須返回1,因為amb()會拋出異常。同理,amb(1, amb)也必須返回1。顯然,amb的實現不能是簡單的檢測每個參數。比如下面這段代碼里的amb(false, true)必須返回true, 不然else里的amb()就會被調用,導致這段代碼拋出異常。
          if amb(falsetrue
             
          return 1
          else
             
          return amb()
          end
          那這種不確定的函數到底有什么用呢?呵呵,用處大了。它正好簡單地模擬了生成-測試:我們有一系列備選答案。我們把這些答案傳給amb,他返回不會失敗的那個。這不正好符合生成-測試的定義么?其實哪怕編程中這個函數也有妙用。比如說下面這道題(SICP4.3.2):

          Baker, Cooper, Fletcher, Miller, 和Smith住在一動5層公寓的不同樓層。Baker不在頂樓住。Cooper不在底樓住。Fletcher既不在頂樓也不在底樓。Miller住的樓層比Cooper住的高。Smith和Fletcher的樓層不相鄰。Fletcher和Cooper的樓層不相鄰。問每個人住的樓層。
          有了amb這個函數,我們可以這么解決這道題(為了方便大多數人,我把Scheme代碼移植到Ruby代碼了。調用amb()變成了amb.choose):
           
          看哈,每一行語句直接對應題目的條件。沒有循環。沒有遞歸。全靠amb這個看似簡單的函數。運行的結果是:
           
          有Scheme運行環境的老大們可以運行下面的代碼(在實現amb以后)。結果一樣。
           
          第一次看到這段代碼,我只覺得背后妖風陣陣。從來沒有想到過,程序能寫到這個地步。每一行語句居然和題目條件切合。比偽代碼還為偽代碼,但偏偏可以運行。那到底amb怎么實現?為什么俺要專門說這個函數呢?原因是這個函數的實現要用到continuation和回溯。當然不用continuation也能實現。比如用Java也行(友情提示:thread + 匿名類)。不過用continuation的方法最為簡潔。Ruby代碼不過10來行。所以我們先簡介一下continuation。
           
          Continuation到底有多重要一直有爭議。Matz在郵件組里說Ruby 2.0會去掉continuation, 因為沒有”compelling use cases”,結果引來一場討論。Avi Brant就力挺continuation,畢竟他的Seaside框架就是基于continuation技術的。對了,Avi在今年的ETech會議上做了報告,批評RoR方法落后。這里有會議筆記,非常有教育意義。簡單說,continuation就是高級goto,好比C下面的setjmp/longjmp + Solaris下的getContext()。它允許程序記住某一點的所有狀態,然后在其它時刻回到那一點,繼續執行。用Ruby舉例(Ruby的callcc和Scheme的用法沒有本質區別):Ruby的函數Kernel#callcc生成一個Continuation對象,并把這個
           
           
          知道了Continuation和回溯的概念,實現Amb也就容易了。因為是Ruby,我用了一個類來封裝變量backtrack_points。這個實現的本質是深度優先搜索。每一步的狀態用Continuation保存起來,回溯時使用。這本寫得很好的Scheme入門書上也有amb的實現。Ruby Quiz提供了類似的代碼,是從同一本書的Scheme代碼直接移植的。不過Scheme不支持return, 所以它的實現用了兩個call/cc。外層的用于模擬return (所謂的escaping continuation)。所以移植的代碼不是那么容易理解。在Ruby里用了return語句后,代碼要干凈清晰得多:

           困。。。代碼的解釋改天繼續。也許這段代碼很直觀不需要解釋呢?哪位老大讀到了這里,說說看?先謝謝了。
           
          主站蜘蛛池模板: 麻阳| 白水县| 阜宁县| 黄大仙区| 太和县| 英吉沙县| 三穗县| 农安县| 册亨县| 项城市| 靖远县| 芜湖县| 广南县| 石楼县| 牡丹江市| 六枝特区| 安溪县| 峨眉山市| 当涂县| 泰兴市| 桃源县| 民权县| 天等县| 成都市| 伽师县| 西林县| 公安县| 南平市| 张北县| 赫章县| 抚顺市| 乐亭县| 桐梓县| 深州市| 营山县| 苏尼特左旗| 无极县| 德州市| 礼泉县| 玛纳斯县| 盘锦市|