第 1 章 可擴(kuò)展語言

2018-02-24 15:54 更新

第 1 章 可擴(kuò)展語言

不久前,如果你問 Lisp 是用來干什么的,很多人會回答說 "人工智能(articial intelligence)" 。事實上,Lisp 和人工智能之間的聯(lián)系只是歷史的偶然。 Lisp 由 John McCarthy 發(fā)明,同樣是他首次提出了 "人工智能" 這一名詞。那時他的學(xué)生和同事用 Lisp 寫程序,于是它就被稱作一種 AI 語言。這個典故在 1980 年代 AI 短暫升溫時又被多次提起,到現(xiàn)在已經(jīng)差不多成了習(xí)慣。

幸運的是, "AI 并非 Lisp 的全部" 的觀點已經(jīng)開始為人們所了解。近年來軟硬件的長足發(fā)展已經(jīng)讓 Lisp 走出了象牙塔:

它目前用于GNUEmacs -- Unix 下最好的文本編輯器;

AutoCAD -- 工業(yè)標(biāo)準(zhǔn)的桌面CAD 程序;

還有Interleaf -- 領(lǐng)先的高端出版系統(tǒng)。

Lisp 在這些程序里的應(yīng)用跟AI 已經(jīng)沒有了任何關(guān)系。

如果 Lisp 不是一種 AI 語言,那它是什么?與其根據(jù)那些使用它的公司來判斷 Lisp ,我們不如直接看看語言本身。有什么是你可以用 Lisp 做到,而其他語言沒法做到的呢? Lisp 的一個最顯著的優(yōu)點是可以對其量身定制,讓它與用它寫的程序相配合。Lisp 本身就是一個 Lisp 程序,Lisp 程序可以表達(dá)成列表,那也是 Lisp 的數(shù)據(jù)結(jié)構(gòu)。

總之,這兩個原則意味著任何用戶都可以為 Lisp 增加新的操作符,而這些新成員和那些內(nèi)置的操作符是沒有區(qū)別的。

1.1 漸進(jìn)式設(shè)計

由于 Lisp 賦予了你自定義操作符的自由,因而你得以隨心所欲地將它塑造成你所需要的語言。

如果你在寫一個文本編輯器,那么可以把 Lisp 轉(zhuǎn)換成專門寫文本編輯器的語言。

如果你在編寫 CAD 程序,那么可以把 Lisp 轉(zhuǎn)換成專用于寫 CAD 程序的語言。

并且如果你還不太清楚你要寫哪種程序,那么用 Lisp 來寫會比較安全。

因為無論你想寫哪種程序,在你寫的時候,Lisp 都可以演變成用于寫那種程序的語言。

你還沒想好要寫哪種程序?一樣可以。對有些人來說,這種說法有點不對勁。這和某種行事方式很不一樣,這種方式有兩步:

(1) 仔細(xì)計劃你打算做的事情,接下來

(2) 去執(zhí)行它。

按照這個邏輯,如果 Lisp 鼓勵你在決定程序應(yīng)該如何工作之前就開始寫程序,它只不過是慫恿你匆忙上馬,草率決定而已。

事實并非如此。先計劃再實施的方法可能是建造水壩或者發(fā)起戰(zhàn)役的方式,但經(jīng)驗并未表明這種方法也適用于寫程序。為什么?也許是因為計算機(jī)的要求太苛刻了。也許是因為程序中的變數(shù)比水壩或者戰(zhàn)役更多?;蛟S老方法不再奏效的原因,是因為舊式的冗余觀念不適用于軟件開發(fā):

如果一座大壩澆鑄了額外的 30% 的混凝土,那是為以后的誤操作留下的裕量,但如果一個程序多做了額外 30% 的工作,那就是一個錯誤。

很難說清原來的辦法為什么會失效,但所有人都心知肚明老辦法不再行之有效。究竟有幾次軟件按時交付過?有經(jīng)驗的程序員知道無論你多小心地計劃一個程序,當(dāng)你著手寫它的時候,之前制定的計劃在某些地方就會變得不夠完美。有時計劃甚至?xí)e得無可救藥。卻很少有"先策劃再實施" 這一方法的受害者站出來質(zhì)疑它的有效性。相反,他們把這都?xì)w咎于人為過失:

只要計劃做的更周詳,所有的問題就都可以避免。

就算是最杰出的程序員,在進(jìn)行具體實現(xiàn)的時候也難免陷入麻煩,因此要人們必須具備那種程度的前瞻性可能過于苛求了。也許這種先策劃再實施的方法可以用另外一種更適合我們自身限制的方法取而代之。

如果有合適的工具,我們完全可以換一種角度看待編程。為什么我們要在具體實現(xiàn)之前計劃好一切呢?盲目啟動一個項目的最大危險是我們可能不小心就使自己陷入困境。但如果存在一種更加靈活的語言,是否能為我們分憂呢?我們可以,而且確實如此。Lisp 的靈活性帶來了全新的編程方式。在 Lisp 中,可以邊寫程序邊做計劃。

為什么要等事后諸葛亮呢?正如 Montaigne 所發(fā)現(xiàn)的那樣,如果要理清自己的思路,試著把它寫下來會是最好的辦法。一旦你能把自己從陷入困境的危險中解脫出來,那你就可以完全駕馭這種可能性。邊設(shè)計邊施工有兩個重要的后果:程序可以花更少的時間去寫,因為當(dāng)你把計劃和實際動手寫放在一起的時候,你總可以把精力集中在一個實際的程序上;然后讓它變得日益完善,因為最終的設(shè)計必定是進(jìn)化的成果。

只要在把握你程序的命運時堅持一個原則:一旦定位錯誤的地方,就立即重寫它,那么最終的產(chǎn)品將會比事先你花幾個星期的時間精心設(shè)計的結(jié)果更加優(yōu)雅。

Lisp 的適應(yīng)能力使這種編程思想成為可能。確實,Lisp 的最大危險是它可能會把你寵壞了。使用 Lisp 一段時間后,你會開始對語言和應(yīng)用程序之間的結(jié)合變得敏感,當(dāng)你回過頭去使用另一種語言時,總會有這樣的感覺:

它無法提供你所需要的靈活性。

1.2 自底向上程序設(shè)計

有一條編程原則由來已久:作為程序的功能性單元不宜過于臃腫。如果程序里某些組件的規(guī)模增長超過了它可讀的程度,它就會成為一團(tuán)亂麻,藏匿其中的錯誤就好像巨型城市里的逃犯那樣難以捉摸。這樣的軟件將難以閱讀,難以測試,調(diào)試起來也會痛苦不堪。

按照這個原則,大型程序必須細(xì)分成小塊,并且程序的規(guī)模越大就應(yīng)該分得越細(xì)。但你怎樣劃分一個程序呢?傳統(tǒng)的觀點被稱為自頂向下的設(shè)計:你說 "這個程序的目的是完成這七件事,那么我就把它分成七個主要的子例程。第一個子例程要做這四件事,所以它將進(jìn)一步細(xì)分成它自己的四個子例程",如此這般。這一過程持續(xù)到整個程序被細(xì)分到合適的粒度 每一部分都足夠大可以做一些實際的事情,但也足夠小到可以作為一個基本單元來理解。

有經(jīng)驗的 Lisp 程序員用另一種不同的方式來細(xì)化他們的程序。和自頂向下的設(shè)計方法類似,他們遵循一種叫做自底向上的設(shè)計原則, 即通過改變語言來適應(yīng)程序。在 Lisp 中,你不僅是根據(jù)語言向下編寫程序,也可以根據(jù)程序向上構(gòu)造語言。在編程的時候你可能會想 " Lisp 要是有這樣或者那樣的操作符就好了。" 那你就可以直接去實現(xiàn)它。之后,你會意識到使用新的操作符也可以簡化程序中另一部分的設(shè)計,如此種種。語言和程序一同演進(jìn)。就像交戰(zhàn)兩國的邊界一樣,語言和程序的界限不斷地移動,直到最終沿著山脈和河流確定下來,這也就是你要解決的問題本身的自然邊界。最后你的程序看起來就好像語言就是為解決它而設(shè)計的。并且當(dāng)語言和程序彼此都配合得非常完美時,你得到的將是清晰、簡短和高效的代碼。

需要強(qiáng)調(diào)的是,自底向上的設(shè)計并不意味著只是換個次序?qū)懗绦?。?dāng)以自底向上的方式工作時,你通常寫出來的程序會徹底改觀。你將得到一個帶有更多抽象操作符的更大的語言,和一個用它寫的更精練的程序,而不是單個的整塊的程序。你得到將是拱而非梁。

在典型的程序中,一旦把那些僅僅是做非邏輯工作的部分抽象掉,剩下的代碼就短小多了;你構(gòu)造的語言越高階,程序從上層邏輯到下層語言的距離就越近。這有幾個好處:

  1. 通過讓語言擔(dān)當(dāng)更多的工作,自底向上設(shè)計產(chǎn)生的程序會更加短小輕快。一個更短小的程序就不必劃分成那么多的組件了,并且更少的組件意味著程序會更易于閱讀和修改。更少的組件也使得著組件之間的連接會更少,因而錯誤發(fā)生的機(jī)會也會相應(yīng)減少。一個機(jī)械設(shè)計師往往努力去減少機(jī)器上運動部件的數(shù)量,同樣有經(jīng)驗的 Lisp 程序員使用自底向上的設(shè)計方法來減小他們程序的規(guī)模和復(fù)雜度。

  2. 自底向上的設(shè)計促進(jìn)了代碼重用。當(dāng)你寫兩個或更多程序時,許多你為第一個程序?qū)懙墓ぞ咭矔χ蟮某绦蜷_發(fā)有幫助。一旦積累下了雄厚的工具基礎(chǔ),寫一個新程序所耗費的精力和從原始(raw) Lisp 環(huán)境白手起家相比,前者可能只是后者的幾分之一。

  3. 自底向上的設(shè)計提高了程序的可讀性。一個這種類型的抽象要求讀者理解一個通用操作符,而一個具體的函數(shù)抽象要求讀者去理解的則是一個專用的子例程。

譯者注:Montaigne,即MichelRyquemdeMontaigne。國內(nèi)一般譯作"蒙田"。他是法國文藝復(fù)興后期重要的人文主義學(xué)者,他曾說過"我本人就是作品的內(nèi)容"。"但是沒人能讀懂你的程序,除非理解了所有新的實用函數(shù)"。要想知道為什么這種認(rèn)識是一種誤解,請參考第4.8 節(jié)。

  1. 由于自底向上的設(shè)計驅(qū)使你總是去關(guān)注代碼中的模式,這種工作方式有助于理清設(shè)計程序時的思路。

如果一個程序中兩個關(guān)系很遠(yuǎn)的組件在形式上很相似,你就會因此注意到這種相似性,然后也許會以更簡單的方式重新設(shè)計程序。

對于其他非 Lisp 的語言來說,自底向上的設(shè)計在某種程度上也是可能的。大家熟悉的庫函數(shù)就是自底向上設(shè)計的一種體現(xiàn)。然而在這方面,Lisp 還能提供比其他語言更強(qiáng)大的威力,而且在以 Lisp 風(fēng)格編程時,擴(kuò)展這門語言的重要性也相應(yīng)提高了,所以 Lisp 不僅是一門不同的編程語言,而且是一種完全不一樣的編程方式。

確實,這種開發(fā)風(fēng)格更適合那種可以小規(guī)模開發(fā)的程序。不過,與此同時,它卻讓一個小組所能做更多的事情。在《人月神話》一書中,F(xiàn)rederickBrooks 提出"一組程序員的生產(chǎn)力并不隨人員的數(shù)量呈線性增長"。

隨著組內(nèi)人數(shù)的增加,個體程序員的生產(chǎn)力將有所下降。 Lisp 編程經(jīng)驗以一種更加令人振奮的方式重申這個定律:隨著組內(nèi)人數(shù)的減少,個體程序員的生產(chǎn)力將會提高。一個小組取得成功的原因,僅僅是因為它的規(guī)模相對較小。如果一個小組能利用 Lisp 帶來的技術(shù)優(yōu)勢,它必定會走向成功。

1.3 可擴(kuò)展軟件

隨著軟件復(fù)雜度的提高,編程的 Lisp 風(fēng)格也變得愈加重要。專業(yè)用戶現(xiàn)在對軟件的要求如此之多以致于我們幾乎無法預(yù)見到他們的所有需求。就算用戶自己也沒辦法預(yù)測到他們所有的需求。但如果我們不能給他們一個現(xiàn)成的軟件,讓它能完成用戶想要的每個功能,那么我們也可以交付一個可擴(kuò)展的軟件。我們把自己的軟件從單單一個程序變成了一門編程語言,然后高級用戶就可以在此基礎(chǔ)上構(gòu)造他們需要的額外特性。

自底向上的設(shè)計很自然地產(chǎn)生了可擴(kuò)展的程序。最簡單的自底向上程序包括兩層:語言和程序。復(fù)雜的程序可以被寫成多個層次,每一層作為其上層的編程語言。如果這一哲學(xué)被一直沿用到最上面的那層,那最上面的這一層對于用戶來說就變成了一門編程語言。這樣一個可擴(kuò)展性體現(xiàn)在每一層次的程序,與那些先按照傳統(tǒng)黑盒方法寫成,事后才加上可擴(kuò)展性的那些系統(tǒng)相比,更有可能成為一門好得多的編程語言。

X-Window 和 TEX 就是遵循這一設(shè)計原則編寫而成的早期典范。在 1980 年代,更強(qiáng)大的硬件使得新一代的 程序能使用 Lisp 作為它們的擴(kuò)展語言。首先是 GNUEmacs,流行的 Unix 文本編輯器。緊接著是 AutoCAD ,第一個把 Lisp 作為擴(kuò)展語言的大型商業(yè)軟件。1991 年 Interleaf 發(fā)布了他們軟件的新版本,它不僅采用 Lisp 作為擴(kuò)展語言,甚至該軟件大部分就是用 Lisp 實現(xiàn)的。

Lisp 這門語言特別適合編寫可擴(kuò)展程序,主要原因是因為它本身就是一個可擴(kuò)展的程序。如果你用 Lisp 寫你的程序以便將這種可擴(kuò)展性轉(zhuǎn)移到用戶那里,你事實上已經(jīng)毫不費力地得到了一個可擴(kuò)展語言。并且用 Lisp 擴(kuò)展 Lisp 程序,和用一個傳統(tǒng)語言做同樣的事情相比,它們的區(qū)別就好比面對面交談和使用書信聯(lián)系的區(qū)別。如果一個程序只是簡單提供了一些供外部程序訪問的方式,以期獲得可擴(kuò)展性,那么我們最樂觀的估計也無非是兩個黑箱之間彼此通過預(yù)先定義好的渠道進(jìn)行通信。在 Lisp 里,這些擴(kuò)展有權(quán)限直接訪問整個底層程序。這并不是說你必須授予用戶你程序中每一個部分的訪問權(quán)限,只是說你現(xiàn)在有機(jī)會決定是否賦給他們這樣的權(quán)限。

當(dāng)權(quán)限的取舍和交互式環(huán)境結(jié)合在一起,你就擁有了處于最佳狀態(tài)的可擴(kuò)展性。任何軟件,如果你想以它為基礎(chǔ),在其上進(jìn)行擴(kuò)展,為己所用,在你心中就好比有了一張非常大,可能過于巨大的完整的藍(lán)圖。要是其中的有些東西不敢確定,該怎么辦?如果原始程序是用 Lisp 開發(fā)的,那就可以交互式地試探它:你可以檢查它的數(shù)據(jù)結(jié)構(gòu);你可以調(diào)用它的函數(shù);你甚至可能去看它最初的源代碼。這種反饋信息讓你能信心百倍地寫程序 去寫更加雄心勃勃的擴(kuò)展,并且會寫得更快。一般而言,交互式環(huán)境可以讓編程更輕松,但它對寫擴(kuò)展的人來說尤其有用。

可擴(kuò)展的程序是一柄雙刃劍,但近來的經(jīng)驗表明,和鈍劍相比,用戶更喜歡雙刃劍。可擴(kuò)展的程序看起來正在流行,無論它們是否暗藏危機(jī)。

1.4 擴(kuò)展 Lisp

有兩種方式可以為 Lisp 增加新的操作符:函數(shù)和宏。在 Lisp 里,你定義的函數(shù)和那些內(nèi)置函數(shù)具有相同的地位。如果想要一個新的改版的mapcar ,那你就可以先自己定義,然后就像使用mapcar 那樣來使用它。

例如,如果有一個函數(shù),你想把從 1 到 10 之間的所有整數(shù)分別傳給它,然后把函數(shù)的返回值組成的列表留下,你可以創(chuàng)建一個新列表然后把它傳給 mapcar :

(mapcar fn
   (do* ((x 1 (1+ x))
      (result (list x) (push x result)))
           ((= x 10) (nreverse result))))

但這樣做既不美觀又沒效率。換種辦法,你也可以定義一個新的映射函數(shù) map1-n (見36 頁),然后像下面那樣調(diào)用它:

(map1-n fn 10)

定義函數(shù)相對而言比較直截了當(dāng)。而用宏來定義新操作符,雖然更通用,但不太容易理解。宏是用來寫程序的程序。這句話意味深長,深入地探究這個問題正是本書的主要目的之一。

深思熟慮地使用宏,可以讓程序驚人的清晰簡潔。這些好處絕非唾手可得。盡管到最后,宏將被視為世上最自然的東西,但最初理解它的時候卻會舉步維艱。部分原因是因為宏比函數(shù)更加一般化,所以編寫的時候要考慮的事情更多。但宏難于理解,最主要的原因是它太另類了。沒有任何一門語言有像 Lisp 宏那樣的東西。所以學(xué)習(xí)宏,可能先要從頭腦中清除從其他語言那里潛移默化接受的先入為主的觀念。這些觀念中,首當(dāng)其沖就是為那些陳詞濫調(diào)所累的程序。憑什么數(shù)據(jù)結(jié)構(gòu)可以變化,并且其中的數(shù)據(jù)可以修改,而程序卻不能呢?在 Lisp 里,程序就是數(shù)據(jù),但其中深意需要假以時日才能體會到。

如果你需要花些時間才能習(xí)慣宏,那么這些時間絕對是值得的。即使像迭代這樣平淡無奇的用法中,宏也可以使程序明顯變得更短小精悍。假設(shè)一個程序需要在某個程序體上從 a 到 b 來迭代x 。Lisp 內(nèi)置的 do 可以用于更加一般的場合。而對于簡單的迭代來說,用它并不能寫出可讀性最好的代碼:

(do ((x a (+ 1 x))) ((> x b)) (print x))

另一方面,假如我們可以只寫成這樣:

(for (x a b) (print x))

宏使這成為可能。用六行代碼(見第104 頁),我們就能把 for 加入到語言中,就好像原裝的一樣。并且正如后面的章節(jié)所展示的,寫個 for 對宏的廣闊天地來說,不過是小試牛刀。

沒有人對你橫加限制,說每次只能為 Lisp 擴(kuò)展一個函數(shù)或是宏。如果需要,你可以在 Lisp 之上構(gòu)造一個完整的語言,然后用它來編寫程序。 Lisp 對于寫編譯器和解釋器來說是極為優(yōu)秀的語言,但它定義新語言的方式和以往完全不同,這種方式通常更加簡潔,而且自然,也更省力:即在原有的 Lisp 基礎(chǔ)上加以修改,成為一門新的語言。這樣,Lisp 中保持不變部分可以在新語言里(例如數(shù)學(xué)計算或者I/O 操作) 得以繼續(xù)沿用,

你只需要實現(xiàn)有變化的那部分(例如控制結(jié)構(gòu))。以這種方式實現(xiàn)的語言被稱為嵌入式語言。

嵌入式語言是自底向上程序設(shè)計的自然產(chǎn)物。Common Lisp 里已經(jīng)有了好幾種這樣的語言。其中最著名的 ??? 將在最后一章里討論。但你也可以定義自己的嵌入式語言。然后就能得到一個完全為你程序度身定制的語言,甚至它們最后看起來跟 Lisp 已經(jīng)截然不同。

1.5 為什么(或說何時) 用 Lisp

這些新的可能性并非來自某一個神奇的源頭。這樣說吧,Lisp 就像一個拱頂。究竟哪一塊楔形石頭(拱石)托起了整個拱呢?這個問題本身就是錯誤的;每一塊都是。和拱一樣,Lisp 是一組相互契合的特性的集合。

你也可以使用 Common Lisp 的series 宏把代碼寫得更簡潔,但那也只能證明同樣的觀點,因為這些宏就是 Lisp 本身的擴(kuò)展。

我們可以列出這些特性中的一部分:動態(tài)存儲分配和垃圾收集、運行時類型系統(tǒng)、函數(shù)對象、生成列表的內(nèi)置解析器、一個接受列表形式的程序的編譯器、交互式環(huán)境等等,但 Lisp 的威力不能單單歸功于它們中的任何一個。是上述這些特性一同造就了 Lisp 編程現(xiàn)在的模樣。

在過去的二十年間,人們的編程方式發(fā)生了變化。其中許多變化 交互式環(huán)境、動態(tài)鏈接,甚至面向?qū)ο蟮某绦蛟O(shè)計,就是一次又一次的嘗試,它們把 Lisp 的一些靈活性帶給其它編程語言。關(guān)于拱頂?shù)哪莻€比喻說明了這些嘗試是怎樣的成功。

眾所周知,Lisp 和 Fortran 是目前仍在使用中的兩門最古老的編程語言。可能更有意思的是,它們在語言設(shè)計的哲學(xué)上代表了截然相反的兩個極端。Fortran 被發(fā)明出來以替代匯編語言。Lisp 被發(fā)明出來表述算法。如此截然不同的意圖產(chǎn)生了迥異的兩門語言,F(xiàn)ortran 使編譯器作者的生活更輕松;而 Lisp 則讓程序員的生活更舒服。自從那時起,大多數(shù)編程語言都落在了兩極之間。Fortran 和 Lisp 它們自己也逐漸在向中間地帶靠攏。Fortran 現(xiàn)在看起來更像Algol 了,而 Lisp 也改掉了它年幼時一些很低效的語言習(xí)慣。

最初的 Fortran 和 Lisp 在某種程度上定義了一個戰(zhàn)場。戰(zhàn)場的一邊的口號是"效率!(并且,還有幾乎不可能實現(xiàn)。)" 在戰(zhàn)場的另一邊,口號是"抽象!(并且不管怎么說,這不是產(chǎn)品級軟件。)" 就好像諸神在冥冥之中決定古希臘戰(zhàn)爭的勝敗那樣,編程語言這場戰(zhàn)爭的結(jié)局取決于硬件。每一年都在往 Lisp 更有利的方向發(fā)展。現(xiàn)在對 Lisp 的爭議聽起來已經(jīng)有點兒像1970 年代早期匯編語言程序員對于高級語言的論點。

問題不再是為什么用 Lisp?而是何時用 Lisp?

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號