Simula 誕生之前的面向對象程序設計
作者在遠足途中思考面向對象程序設計的本體論問題。
自從希基發表演講之後,人們對函數式編程語言的興趣不斷提升,主流的面向對象編程語言也大多都採用了函數式編程語言。儘管如此,大多數程序員依舊沿用自己的老一套,繼續將對象實例化,不斷改變其狀態。這些人長此以往,很難做到用不同的視角看待編程。
我曾經想寫一篇關於 Simula 的文章,大概會寫到我們今天所熟知的面向對象的理念是何時又是如何應用到程序語言之中的。但是,我覺得寫當初的 Simula 與如今的面向對象程序設計的 迥然不同之處,會更有趣一些,這我敢打包票。畢竟,我們現在熟知的面向對象程序設計還未完全成型。Simula 有兩個主要版本:Simula I 和 Simula 67。Simula 67 為世界帶來了 類 、 類的繼承 以及 虛擬方法 ;但 Simula I 是一個初稿,它實驗了如何能夠將數據和進程捆綁起來的其他設想。Simula I 的模型不是希基提出的函數式模型,不過這一模型關注的是隨時間展開的 進程,而非有著隱藏狀態的對象之間的相互作用。如果 Simula 67 採用了 Simula I 的理念,那麼我們如今所知的面向對象程序設計可能會大有不同——這類偶然性啟示我們,不要想著現在的程序設計範式會一直佔據主導地位。
從 Simula 0 到 Simula 67
Simula 是由兩位挪威人 克里斯汀·尼加德 和 奧利-約翰·達爾 創建的。
20 世紀 50 年代末,尼加德受雇於 挪威防務科學研究中心 (NDRE),該研究中心隸屬於挪威軍方。在那裡,他負責設計 蒙特卡洛模擬方法 ,用於核反應堆設計與操作研究。最初,那些模擬實驗是由人工完成的;後來,實驗在 Ferranti Mercury 電腦 [1] 上編入程序運行。尼加德隨後發現,將這些模擬實驗輸入電腦需要一種更有效的方式。
尼加德設計的這種模擬實驗就是人們所知的「 離散事件模型 」,這種模擬記錄了一系列事件隨著時間改變系統狀態的進程。但是問題的關鍵在於模擬可以從一個事件跳躍到另一個事件中,因為事件是離散的,事件之間的系統不存在任何變化。根據尼加德和達爾在 1966 年發表的一篇關於 Simula 的論文,這種模型被迅速應用於「神經網路、通信系統、交通流量、生產系統、管理系統、社會系統等」 [2] 領域的分析。因此,尼加德認為,其他人描述模擬實驗時,可能也需要更高層級的模型。於是他開始物色人才,幫助他完成他稱之為「 模擬語言 」或者「 蒙特卡洛編譯器 」的項目 [3] 。
達爾當時也受雇於挪威防務科學研究中心,專攻語言設計,此時也加入了尼加德的項目,扮演「沃茲尼亞克」的角色(LCTT 譯註:指蘋果公司聯合創始人斯蒂夫·蓋瑞·沃茲尼亞克)。在接下來一年左右的時間,尼加德和達爾攜手開發了 Simula 0 語言。 [4] 這一語言的早期版本僅僅是在 ALGOL 60 基礎上進行的較小拓展,當時也只是打算將其用作預處理程序而已。當時的語言要比後來的編程語言抽象得多,其基本語言結構是「 車站 」與「 乘客 」,這些結構可以用於針對具體某些離散事件網路建立模型。尼加德和達爾給出了一個模擬飛機離港的例子。 [5] 但是尼加德和達爾最後想出了一個更加通用的語言結構,可以同時表示「車站」和「乘客」,也可以為更廣泛的模擬建立模型。這是兩個主要的概括,它改變了 Simula 作為 ALGOL 專屬包的定位,使其轉變為通用編程語言。
Simula I 沒有「 車站 」和「 乘客 」的語言結構,但它可以通過使用「 進程 」再現這些結構。(LCTT 譯註:此處使用的「進程」,與當前計算機中用來指代一個已執行程序的實體的概念不同,大致上,你可以將本文中所說的「進程」理解為一種「對象」。)一個進程包含大量數據屬性,這些屬性與作為進程的 操作規程 的單個行為相聯繫。你可能會把進程當作是只有單個方法的對象,比如 run()
之類的。不過,這種類比並不全面,因為每個進程的操作規程都可以隨時暫停、隨時恢復,因為這種操作規程屬於 協程 的一種。Simula I 程序會將系統建立為一套進程的模型,在概念上這些進程並行運行。實際上,一個時間點上能稱為「當前進程」的只有一個進程。但是,一旦某個進程暫停運行,那麼下一個進程就會自動接替它的位置。隨著模擬的運行,Simula 會保持一個 「 事件通知 」 的時間線,跟蹤記錄每個進程恢復的時間。為了恢復暫停運行的進程,Simula 需要記錄多個 調用棧 的情況。這就意味著 Simula 無法再作為 ALGOL 的預處理程序了,因為 ALGOL 只有一個 調用棧 。於是,尼加德和達爾下定決心,開始編寫自己的編譯器。
尼加德和達爾在介紹該系統的論文中,藉助圖示,通過模擬一個可用機器數量有限的工廠,闡明了其用法。 [6] 在該案例中,進程就好比訂單:通過尋找可用的機器,訂單得以發出;如果沒有可用的機器,訂單就會擱置;而一旦有機器空出來,訂單就會執行下去。有一個訂單進程的定義,用來實例化若干種不同的訂單實例,不過這些實例並未調用任何方法。該程序的主體僅僅是創建進程,並使其運行。
歷史上第一個 Simula I 編譯器發佈於 1965 年。尼加德和達爾在離開挪威防務科學研究中心之後,就進入了 挪威計算機中心 工作,Simula I 也是在這裡日漸流行起來的。當時,Simula I 在 UNIVAC 公司的計算機和 Burroughs 公司的 B5500 計算機上均可執行。 [7] 尼加德和達爾兩人與一家名為 ASEA 的瑞典公司達成了諮詢協議,運用 Simula 模擬加工車間。但是,尼加德和達爾隨後就意識到 Simula 也可以寫一些和模擬完全不搭邊的程序。
奧斯陸大學 教授 斯坦因·克羅達爾 曾寫過關於 Simula 的發展史,稱「真正能夠促使新開發的通用語言快速發展的催化劑」就是 一篇題為 《記錄處理》 的論文,作者是英國計算機科學家 查爾斯·安東尼·理查德·霍爾 。 [8] 假如你現在讀霍爾的這篇論文,你就不會懷疑這句話。當人們談及面向對象語言的發展史時,一定會經常提起霍爾的大名。以下內容摘自霍爾的《記錄處理》一文:
該方案設想,在程序執行期間,計算機內部存在任意數量的記錄,每條記錄都代表著程序員在過去、現在或未來所需的某個對象。程序對現有記錄的數量保持動態控制,並可以根據當前任務的要求創建新的記錄或刪除現有記錄。
計算機中的每條記錄都必須屬於數量有限但互不重合的記錄類型中的一類;程序員可以根據需要聲明儘可能多的記錄類型,並藉助標識符為各個類型命名。記錄類型的命名可能是普通辭彙,比如「牛」、「桌子」以及「房子」,同時,歸屬於這些類型的記錄分別代表一頭「牛」、一張「桌子」以及一座「房子」。
霍爾在這片論文中並未提到子類的概念,但是達爾由衷地感謝霍爾,是他引導了兩人發現了這一概念。 [9] 尼加德和達爾注意到 Simula I 的進程通常具有相同的元素,所以引入父類來執行共同元素就會非常方便。這也強化了「進程」這一概念本身可以用作父類的可能性,也就是說,並非每種類型都必須用作只有單個操作規程的進程。這就是 Simula 語言邁向通用化的第二次飛躍,此時,Simula 67 真正成為了通用編程語言。正是如此變化讓尼加德和達爾短暫地萌生了給 Simula 改名的想法,想讓人們意識到 Simula 不僅僅可以用作模擬。 [10] 不過,考慮到 「Simula」這個名字的知名度已經很高了,另取名字恐怕會帶來不小的麻煩。
1967 年,尼加德和達爾與 控制數據公司 簽署協議,著手開發Simula 的新版本:Simula 67。同年六月份的一場會議中,來自控制數據公司、奧斯陸大學以及挪威計算機中心的代表與尼加德和達爾兩人會面,意在為這門新語言制定標準與規範。最終,會議發布了 《Simula 67 通用基礎語言》,確定了該語言的發展方向。
Simula 67 編譯器的開發由若干家供應商負責。 Simula 用戶協會 (ASU)也隨後成立,並於每年舉辦年會。不久,Simula 67 的用戶就遍及了 23 個國家。 [11]
21 世紀的 Simula 語言
人們至今還記得 Simula,是因為後來那些取代它的編程語言都受到了它的巨大影響。到了今天,你很難找到還在使用 Simula 寫程序的人,但是這並不意味著 Simula 已經從這個世界上消失了。得益於 GNU cim,人們在今天依然能夠編寫和運行 Simula 程序。
cim 編譯器遵循 1986 年修訂後的 Simula 標準,基本上也就是 Simula 67 版本。你可以用它編寫類、子類以及虛擬方法,就像是在使用 Simula 67 一樣。所以,用 Python 或 Ruby 輕鬆寫出短短几行面向對象的程序,你照樣也可以用 cim 寫出來:
! dogs.sim ;
Begin
Class Dog;
! The cim compiler requires virtual procedures to be fully specified ;
Virtual: Procedure bark Is Procedure bark;;
Begin
Procedure bark;
Begin
OutText("Woof!");
OutImage; ! Outputs a newline ;
End;
End;
Dog Class Chihuahua; ! Chihuahua is "prefixed" by Dog ;
Begin
Procedure bark;
Begin
OutText("Yap yap yap yap yap yap");
OutImage;
End;
End;
Ref (Dog) d;
d :- new Chihuahua; ! :- is the reference assignment operator ;
d.bark;
End;
你可以按照下面代碼執行程序的編譯與運行:
$ cim dogs.sim
Compiling dogs.sim:
gcc -g -O2 -c dogs.c
gcc -g -O2 -o dogs dogs.o -L/usr/local/lib -lcim
$ ./dogs
Yap yap yap yap yap yap
(你可能會注意到,cim 先將 Simula 語言編譯為 C 語言,然後傳遞給 C 語言編譯器。)
這就是 1967 年的面向對象程序設計,除了語法方面的不同,和 2019 年的面向對象程序設計並無本質區別。如果你同意我的這一觀點,你也就懂得了為什麼人們會認為 Simula 在歷史上是那麼的重要。
不過,我更想介紹一下 Simula I 的核心概念——進程模型。Simula 67 保留了進程模型,不過只有在使用 Process
類 和 Simulation
塊的時候才能調用。
為了表現出進程是如何運行的,我決定模擬下述場景。想像一下,有這麼一座住滿了村民的村莊,村莊的旁邊有條小河邊,小河裡有很多的魚。但是,村裡的村民卻只有一條魚竿。村民們胃口很大,每隔一個小時就餓了。他們一餓,就會拿著魚竿去釣魚。如果一位村民正在等魚竿,另一位村民自然也用不了。這樣一來,村民們就會為了釣魚排起長長的隊伍。假如村民要等五、六分鐘才能釣到一條魚,那麼這樣等下去,村民們的身體狀況就會變得越來越差。再假如,一位村民已經到了骨瘦如柴的地步,最後他可能就會餓死。
這個例子多少有些奇怪,雖然我也不說不出來為什麼我腦袋裡最先想到的是這樣的故事,但是就這樣吧。我們把村民們當作 Simula 的各個進程,觀察在有著四個村民的村莊里,一天的模擬時間內會發生什麼。
完整程序可以通過此處 GitHub Gist 的鏈接獲取。
我把輸出結果的最後幾行放在了下面。我們來看看一天里最後幾個小時發生了什麼:
1299.45: 王五餓了,要了魚竿。
1299.45: 王五正在釣魚。
1311.39: 王五釣到了一條魚。
1328.96: 趙六餓了,要了魚竿。
1328.96: 趙六正在釣魚。
1331.25: 李四餓了,要了魚竿。
1340.44: 趙六釣到了一條魚。
1340.44: 李四餓著肚子等著魚竿。
1340.44: 李四在等魚竿的時候餓死了。
1369.21: 王五餓了,要了魚竿。
1369.21: 王五正在釣魚。
1379.33: 王五釣到了一條魚。
1409.59: 趙六餓了,要了魚竿。
1409.59: 趙六正在釣魚。
1419.98: 趙六釣到了一條魚。
1427.53: 王五餓了,要了魚竿。
1427.53: 王五正在釣魚。
1437.52: 王五釣到了一條魚。
可憐的李四最後餓死了,但是他比張三要長壽,因為張三還沒到上午 7 點就餓死了。趙六和王五現在一定過得很好,因為需要魚竿的就只剩下他們兩個了。
這裡,我要說明,這個程序最重要的部分只是創建了進程(四個村民),並讓它們運行下去。各個進程操作對象(魚竿)的方式與我們今天對對象的操作方式相同。但是程序的主體部分並沒有調用任何方法,也沒有修改進程的任何屬性。進程本身具有內部狀態,但是這種內部狀態的改變只有進程自身才能做到。
在這個程序中,仍然有一些欄位發生了變化,這類程序設計無法直接解決純函數式編程所能解決的問題。但是正如克羅達爾所注意到的那樣,「這一機制引導進行模擬的程序員為底層系統建立模型,生成一系列進程,每個進程表示了系統內的自然事件順序。」 [12] 我們不是主要從名詞或行動者(對其他對象做事的對象)的角度來思考正在進行的進程。我們可以將程序的總控制權交予 Simula 的事件通知系統,克羅達爾稱其為 「 時間管理器 」。因此,儘管我們仍然在適當地改變進程,但是沒有任何進程可以假設其他進程的狀態。每個進程只能間接地與其他進程進行交互。
這種模式如何用以編寫編譯器、HTTP 伺服器以及其他內容,尚且無法確定。(另外,如果你在 Unity 遊戲引擎上編寫過遊戲,就會發現兩者十分相似。)我也承認,儘管我們有了「時間管理器」,但這可能並不完全是希基的意思,他說我們在程序中需要一個明確的時間概念。(我認為,希基想要的類似於 阿達·洛芙萊斯 用於區分一個變數隨時間變化產生的不同數值的上標符號。)儘管如此,我們可以發現,面向對象程序設計前期的設計方式與我們今天所習慣的面向對象程序設計並非完全一致,我覺得這一點很有意思。我們可能會理所當然地認為,面向對象程序設計的方式千篇一律,即程序就是對事件的一長串記錄:某個對象以特定順序對其他對象產生作用。Simula I 的進程系統表明,面向對象程序設計的方式不止一種。仔細想一下,函數式語言或許是更好的設計方式,但是 Simula I 的發展告訴我們,現代面向對象程序設計被取代也很正常。
如果你喜歡這篇文章,歡迎關注推特 @TwoBitHistory,也可通過 RSS feed 訂閱,獲取最新文章(每四周更新一篇)。
- Jan Rune Holmevik, 「The History of Simula,」 accessed January 31, 2019, http://campus.hesge.ch/daehne/2004-2005/langages/simula.htm. ↩︎
- Ole-Johan Dahl and Kristen Nygaard, 「SIMULA—An ALGOL-Based Simulation Langauge,」 Communications of the ACM 9, no. 9 (September 1966): 671, accessed January 31, 2019, http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.95.384&rep=rep1&type=pdf. ↩︎
- Stein Krogdahl, 「The Birth of Simula,」 2, accessed January 31, 2019, http://heim.ifi.uio.no/~steinkr/papers/HiNC1-webversion-simula.pdf. ↩︎
- 出處同上。 ↩︎
- Ole-Johan Dahl and Kristen Nygaard, 「The Development of the Simula Languages,」 ACM SIGPLAN Notices 13, no. 8 (August 1978): 248, accessed January 31, 2019, https://hannemyr.com/cache/knojd_acm78.pdf. ↩︎
- Dahl and Nygaard (1966), 676. ↩︎
- Dahl and Nygaard (1978), 257. ↩︎
- Krogdahl, 3. ↩︎
- Ole-Johan Dahl, 「The Birth of Object-Orientation: The Simula Languages,」 3, accessed January 31, 2019, http://www.olejohandahl.info/old/birth-of-oo.pdf. ↩︎
- Dahl and Nygaard (1978), 265. ↩︎
- Holmevik. ↩︎
- Krogdahl, 4. ↩︎
via: https://twobithistory.org/2019/01/31/simula.html
作者:Two-Bit History 選題:lujun9972 譯者:aREversez 校對:校對者ID
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive