深入理解Lisp的本質(zhì)
簡(jiǎn)介
最初在web的某些角落偶然看到有人贊美Lisp時(shí), 我那時(shí)已經(jīng)是一個(gè)頗有經(jīng)驗(yàn)的程序員。在我的履歷上, 掌握的語言范圍相當(dāng)廣泛, 象C++, Java, C#主流語言等等都不在話下,我覺得我差不多知道所有的有關(guān)編程語言的事情。對(duì)待編程語言的問題上, 我覺得自己不太會(huì)遇到什么大問題。其實(shí)我大錯(cuò)特錯(cuò)了。
我試著學(xué)了一下Lisp, 結(jié)果馬上就撞了墻。我被那些范例代碼嚇壞了。我想很多初次接觸Lisp語言的人, 一定也有過類似的感受。Lisp的語法太次了。一個(gè)語言的發(fā)明人, 居然不肯用心弄出一套漂亮的語法, 那誰還會(huì)愿意學(xué)它。反正, 我是確確實(shí)實(shí)被那些難看的無數(shù)的括號(hào)搞蒙了。
回過神來之后, 我和Lisp社區(qū)的那伙人交談, 訴說我的沮喪心情。結(jié)果, 立馬就有一大套理論砸過來, 這套理論在Lisp社區(qū)處處可見, 幾成慣例。比如說: Lisp的括號(hào)只是表面現(xiàn)象; Lisp的代碼和數(shù)據(jù)的表達(dá)方式?jīng)]有差別, 而且比XML語法高明許多, 所以有無窮的好處; Lisp有強(qiáng)大無比的元語言能力, 程序員可以寫出自我維護(hù)的代碼; Lisp可以創(chuàng)造出針對(duì)特定應(yīng)用的語言子集; Lisp的運(yùn)行時(shí)和編譯時(shí)沒有明確的分界; 等等, 等等, 等等。這么長(zhǎng)的贊美詞雖然看起來相當(dāng)動(dòng)人, 不過對(duì)我毫無意義。沒人能給我演示這些東西是如何應(yīng)用的, 因?yàn)檫@些東西一般來說只有在大型系統(tǒng)才會(huì)用到。我爭(zhēng)辯說, 這些東西傳統(tǒng)語言一樣辦得到。在和別人爭(zhēng)論了數(shù)個(gè)小時(shí)之后, 我最終還是放棄了學(xué)Lisp的念頭。為什么要花費(fèi)幾個(gè)月的時(shí)間學(xué)習(xí)語法這么難看的語言呢? 這種語言的概念這么晦澀, 又沒什么好懂的例子。也許這語言不是該我這樣的人學(xué)的。
幾個(gè)月來, 我承受著這些Lisp辯護(hù)士對(duì)我心靈的重壓。我一度陷入了困惑。我認(rèn)識(shí)一些絕頂聰明的人, 我對(duì)他們相當(dāng)尊敬, 我看到他們對(duì)Lisp的贊美達(dá)到了宗教般的高度。這就是說, Lisp中一定有某種神秘的東西存在, 我不能忍受自己對(duì)此的無知, 好奇心和求知欲最終不可遏制。我于是咬緊牙關(guān)埋頭學(xué)習(xí)Lisp, 經(jīng)過幾個(gè)月的時(shí)間費(fèi)勁心力的練習(xí), 終于,我看到了那無窮無盡的泉水的源頭。在經(jīng)過脫胎換骨的磨練之后, 在經(jīng)過七重地獄的煎熬之后, 終于, 我明白了。
頓悟在突然之間來臨。曾經(jīng)許多次, 我聽到別人引用雷蒙德(譯者注: 論文<<大教堂和市集>>的作者, 著名的黑客社區(qū)理論家)的話: “Lisp語言值得學(xué)習(xí)。當(dāng)你學(xué)會(huì)Lisp之后, 你會(huì)擁有深刻的體驗(yàn)。就算你平常并不用Lisp編程, 它也會(huì)使你成為更加優(yōu)秀的程序員”。過去, 我根本不懂這些話的含義, 我也不相信這是真的??墒乾F(xiàn)在我懂得了。這些話蘊(yùn)含的真理遠(yuǎn)遠(yuǎn)超過我過去的想像。我內(nèi)心體會(huì)到一種神圣的情感, 一瞬間的頓悟, 幾乎使我對(duì)電腦科學(xué)的觀念發(fā)生了根本的改變。
頓悟的那一刻, 我成了Lisp的崇拜者。我體驗(yàn)到了宗教大師的感受: 一定要把我的知識(shí)傳布開來, 至少要讓10個(gè)迷失的靈魂得到拯救。按照通常的辦法, 我把這些道理(就是剛開始別人砸過來的那一套, 不過現(xiàn)在我明白了真實(shí)的含義)告訴旁人。結(jié)果太令人失望了,只有少數(shù)幾個(gè)人在我堅(jiān)持之下, 發(fā)生了一點(diǎn)興趣, 但是僅僅看了幾眼Lisp代碼, 他們就退卻了。照這樣的辦法, 也許費(fèi)數(shù)年功夫能造就了幾個(gè)Lisp迷, 但我覺得這樣的結(jié)果太差強(qiáng)人意了, 我得想一套有更好的辦法。
我深入地思考了這個(gè)問題。是不是Lisp有什么很艱深的東西, 令得那么多老練的程序員都不能領(lǐng)會(huì)? 不是, 沒有任何絕對(duì)艱深的東西。因?yàn)槲夷芘? 我相信其他人也一定能。那么問題出在那里? 后來我終于找到了答案。我的結(jié)論就是, 凡是教人學(xué)高級(jí)概念, 一定要從他已經(jīng)懂得的東西開始。如果學(xué)習(xí)過程很有趣, 學(xué)習(xí)的內(nèi)容表達(dá)得很恰當(dāng), 新概念就會(huì)變得相當(dāng)直觀。這就是我的答案。所謂元編程, 所謂數(shù)據(jù)和代碼形式合一, 所謂自修改代碼, 所謂特定應(yīng)用的子語言, 所有這些概念根本就是同族概念, 彼此互為解釋, 肯定越講越不明白。還是從實(shí)際的例子出發(fā)最有用。
我把我的想法說給Lisp程序員聽, 遭到了他們的反對(duì)。”這些東西本身當(dāng)然不可能用熟悉的知識(shí)來解釋, 這些概念完全與眾不同, 你不可能在別人已有的經(jīng)驗(yàn)里找到類似的東西”,可是我認(rèn)為這些都是遁詞。他們又反問我, “你自己為啥不試一下?” 好吧, 我來試一下。這篇文章就是我嘗試的結(jié)果。我要用熟悉的直觀的方法來解釋Lisp, 我希望有勇氣的人讀完它, 拿杯飲料, 深呼吸一下, 準(zhǔn)備被搞得暈頭轉(zhuǎn)向。來吧, 愿你獲得大能。
重新審視XML
千里之行始于足下。讓我們的***步從XML開始。可是XML已經(jīng)說得更多的了, 還能有什么新意思可說呢? 有的。XML自身雖然談?wù)劜簧嫌腥? 但是XML和Lisp的關(guān)系卻相當(dāng)有趣。XML和Lisp的概念有著驚人的相似之處。XML是我們通向理解Lisp的橋梁。好吧, 我們且把XML當(dāng)作活馬醫(yī)。讓我們拿好手杖, 對(duì)XML的無人涉及的荒原地帶作一番探險(xiǎn)。我們要從一個(gè)全新的視角來考察這個(gè)題目。
表面上看, XML是一種標(biāo)準(zhǔn)化語法, 它以適合人閱讀的格式來表達(dá)任意的層次化數(shù)據(jù)(hirearchical data)。象任務(wù)表(to-do list), 網(wǎng)頁, 病歷, 汽車保險(xiǎn)單, 配置文件等等, 都是XML用武的地方。比如我們拿任務(wù)表做例子:
- <todo name="housework">
- <item priority="high">Clean the house.</item>
- <item priority="medium">Wash the dishes.</item>
- <item priority="medium">Buy more soap.</item>
- </todo>
解析這段數(shù)據(jù)時(shí)會(huì)發(fā)生什么情況? 解析之后的數(shù)據(jù)在內(nèi)存中怎樣表示? 顯然, 用樹來表示這種層次化數(shù)據(jù)是很恰當(dāng)?shù)?。說到底, XML這種比較容易閱讀的數(shù)據(jù)格式, 就是樹型結(jié)構(gòu)數(shù)據(jù)經(jīng)過序列化之后的結(jié)果。任何可以用樹來表示的數(shù)據(jù), 同樣可以用XML來表示, 反之亦然。希望你能懂得這一點(diǎn), 這對(duì)下面的內(nèi)容極其重要。
再進(jìn)一步。還有什么類型的數(shù)據(jù)也常用樹來表示? 無疑列表(list)也是一種。上過編譯課吧? 還模模糊糊記得一點(diǎn)吧? 源代碼在解析之后也是用樹結(jié)構(gòu)來存放的, 任何編譯程序都會(huì)把源代碼解析成一棵抽象語法樹, 這樣的表示法很恰當(dāng), 因?yàn)樵创a就是層次結(jié)構(gòu)的:函數(shù)包含參數(shù)和代碼塊, 代碼快包含表達(dá)式和語句, 語句包含變量和運(yùn)算符等等。
我們已經(jīng)知道, 任何樹結(jié)構(gòu)都可以輕而易舉的寫成XML, 而任何代碼都會(huì)解析成樹, 因此,任何代碼都可以轉(zhuǎn)換成XML, 對(duì)不對(duì)? 我舉個(gè)例子, 請(qǐng)看下面的函數(shù):
- int add(int arg1, int arg2)
- {
- return arg1+arg2;
- }
能把這個(gè)函數(shù)變成對(duì)等的XML格式嗎? 當(dāng)然可以。我們可以用很多種方式做到, 下面是其中的一種, 十分簡(jiǎn)單:
- <define-function return-type="int" name="add">
- <arguments>
- <argument type="int">arg1</argument>
- <argument type="int">arg2</argument>
- </arguments>
- <body>
- <return>
- <add value1="arg1" value2="arg2" />
- </return>
- </body>
- </define>
這個(gè)例子非常簡(jiǎn)單, 用哪種語言來做都不會(huì)有太大問題。我們可以把任何程序碼轉(zhuǎn)成XML,也可以把XML轉(zhuǎn)回到原來的程序碼。我們可以寫一個(gè)轉(zhuǎn)換器, 把Java代碼轉(zhuǎn)成XML, 另一個(gè)轉(zhuǎn)換器把XML轉(zhuǎn)回到Java。一樣的道理, 這種手段也可以用來對(duì)付C++(這樣做跟發(fā)瘋差不多么??墒堑拇_有人在做, 看看GCC-XML(http://www.gccxml.org)就知道了)。進(jìn)一步說,凡是有相同語言特性而語法不同的語言, 都可以把XML當(dāng)作中介來互相轉(zhuǎn)換代碼。實(shí)際上幾乎所有的主流語言都在一定程度上滿足這個(gè)條件。我們可以把XML作為一種中間表示法,在兩種語言之間互相譯碼。比方說, 我們可以用Java2XML把Java代碼轉(zhuǎn)換成XML, 然后用XML2CPP再把XML轉(zhuǎn)換成C++代碼, 運(yùn)氣好的話, 就是說, 如果我們小心避免使用那些C++不具備的Java特性的話, 我們可以得到完好的C++程序。這辦法怎么樣, 漂亮吧?
這一切充分說明, 我們可以把XML作為源代碼的通用存儲(chǔ)方式, 其實(shí)我們能夠產(chǎn)生一整套使用統(tǒng)一語法的程序語言, 也能寫出轉(zhuǎn)換器, 把已有代碼轉(zhuǎn)換成XML格式。如果真的采納這種辦法, 各種語言的編譯器就用不著自己寫語法解析了, 它們可以直接用XML的語法解析來直接生成抽象語法樹。
說到這里你該問了, 我們研究了這半天XML, 這和Lisp有什么關(guān)系呢? 畢竟XML出來之時(shí),Lisp早已經(jīng)問世三十年了。這里我可以保證, 你馬上就會(huì)明白。不過在繼續(xù)解釋之前, 我們先做一個(gè)小小的思維練習(xí)。看一下上面這個(gè)XML版本的add函數(shù)例子, 你怎樣給它分類,是代碼還是數(shù)據(jù)? 不用太多考慮都能明白, 把它分到哪一類都講得通。它是XML, 它是標(biāo)準(zhǔn)格式的數(shù)據(jù)。我們也知道, 它可以通過內(nèi)存中的樹結(jié)構(gòu)來生成(GCC-XML做的就是這個(gè)事情)。它保存在不可執(zhí)行的文件中。我們可以把它解析成樹節(jié)點(diǎn), 然后做任意的轉(zhuǎn)換。顯而易見, 它是數(shù)據(jù)。不過且慢, 雖然它語法有點(diǎn)陌生, 可它又確確實(shí)實(shí)是一個(gè)add函數(shù),對(duì)吧? 一旦經(jīng)過解析, 它就可以拿給編譯器編譯執(zhí)行。我們可以輕而易舉寫出這個(gè)XML代碼解釋器, 并且直接運(yùn)行它?;蛘呶覀円部梢园阉g成Java或C++代碼, 然后再編譯運(yùn)行。所以說, 它也是代碼。
我們說到那里了? 不錯(cuò), 我們已經(jīng)發(fā)現(xiàn)了一個(gè)有趣的關(guān)鍵之點(diǎn)。過去被認(rèn)為很難解的概念已經(jīng)非常直觀非常簡(jiǎn)單的顯現(xiàn)出來。代碼也是數(shù)據(jù), 并且從來都是如此。這聽起來瘋瘋癲癲的, 實(shí)際上卻是必然之事。我許諾過會(huì)以一種全新的方式來解釋Lisp, 我要重申我的許諾。但是我們此刻還沒有到預(yù)定的地方, 所以還是先繼續(xù)上邊的討論。
剛才我說過, 我們可以非常簡(jiǎn)單地實(shí)現(xiàn)XML版的add函數(shù)解釋器, 這聽起來好像不過是說說而已。誰真的會(huì)動(dòng)手做一下呢? 未必有多少人會(huì)認(rèn)真對(duì)待這件事。隨便說說, 并不打算真的去做, 這樣的事情你在生活中恐怕也遇到吧。你明白我這樣說的意思吧, 我說的有沒有打動(dòng)你? 有哇, 那好, 我們繼續(xù)。
#p#
重新審視Ant
我們現(xiàn)在已經(jīng)來到了月亮背光的那一面, 先別忙著離開。再探索一下, 看看我們還能發(fā)現(xiàn)什么東西。閉上眼睛, 想一想2000年冬天的那個(gè)雨夜, 一個(gè)名叫James Duncan Davidson的杰出的程序員正在研究Tomcat的servlet容器。那時(shí), 他正小心地保存好剛修改過的文件, 然后執(zhí)行make。結(jié)果冒出了一大堆錯(cuò)誤, 顯然有什么東西搞錯(cuò)了。經(jīng)過仔細(xì)檢查, 他想, 難道是因?yàn)閠ab前面加了個(gè)空格而導(dǎo)致命令不能執(zhí)行嗎? 確實(shí)如此。老是這樣, 他真的受夠了。烏云背后的月亮給了他啟示, 他創(chuàng)建了一個(gè)新的Java項(xiàng)目, 然后寫了一個(gè)簡(jiǎn)單但是十分有用的工具, 這個(gè)工具巧妙地利用了Java屬性文件中的信息來構(gòu)造工程, 現(xiàn)在James可以寫makefile的替代品, 它能起到相同的作用, 而形式更加優(yōu)美, 也不用擔(dān)心有makefile那樣可恨的空格問題。這個(gè)工具能夠自動(dòng)解釋屬性文件, 然后采取正確的動(dòng)作來編譯工程。真是簡(jiǎn)單而優(yōu)美。
(作者注: 我不認(rèn)識(shí)James, James也不認(rèn)識(shí)我, 這個(gè)故事是根據(jù)網(wǎng)上關(guān)于Ant歷史的帖子虛構(gòu)的)
使用Ant構(gòu)造Tomcat之后幾個(gè)月, 他越來越感到Java的屬性文件不足以表達(dá)復(fù)雜的構(gòu)造指令。文件需要檢出, 拷貝, 編譯, 發(fā)到另外一臺(tái)機(jī)器, 進(jìn)行單元測(cè)試。要是出錯(cuò), 就發(fā)郵件給相關(guān)人員, 要是成功, 就繼續(xù)在盡可能高層的卷(volumn)上執(zhí)行構(gòu)造。追蹤到***,卷要回復(fù)到最初的水平上。確實(shí), Java的屬性文件不夠用了, James需要更有彈性的解決方案。他不想自己寫解析器(因?yàn)樗M幸粋€(gè)具有工業(yè)標(biāo)準(zhǔn)的方案)。XML看起來是個(gè)不錯(cuò)的選擇。他花了幾天工夫把Ant 移植到XML,于是,一件偉大的工具誕生了。
Ant是怎樣工作的?原理非常簡(jiǎn)單。Ant把包含有構(gòu)造命令的XML文件(算代碼還是算數(shù)據(jù),你自己想吧),交給一個(gè)Java程序來解析每一個(gè)元素,實(shí)際情況比我說的還要簡(jiǎn)單得多。一個(gè)簡(jiǎn)單的XML指令會(huì)導(dǎo)致具有相同名字的Java類裝入,并執(zhí)行其代碼。
- <copy todir="../new/dir">
- <fileset dir="src_dir" />
- </copy>
這段文字的含義是把源目錄復(fù)制到目標(biāo)目錄,Ant會(huì)找到一個(gè)”copy”任務(wù)(實(shí)際上就是一個(gè)Java類), 通過調(diào)用Java的方法來設(shè)置適當(dāng)參數(shù)(todir和fileset),然后執(zhí)行這個(gè)任務(wù)。Ant帶有一組核心類, 可以由用戶任意擴(kuò)展, 只要遵守若干約定就可以。Ant找到這些類,每當(dāng)遇到XML元素有同樣的名字, 就執(zhí)行相應(yīng)的代碼。過程非常簡(jiǎn)單。Ant做到了我們前面所說的東西: 它是一個(gè)語言解釋器, 以XML作為語法, 把XML元素轉(zhuǎn)譯為適當(dāng)?shù)腏ava指令。我們可以寫一個(gè)”add”任務(wù), 然后, 當(dāng)發(fā)現(xiàn)XML中有add描述的時(shí)候, 就執(zhí)行這個(gè)add任務(wù)。由于Ant是非常流行的項(xiàng)目, 前面展示的策略就顯得更為明智。畢竟, 這個(gè)工具每天差不多有幾千家公司在使用。
到目前為之, 我還沒有說Ant在解析XML時(shí)所遇到困難。你也不用麻煩去它的網(wǎng)站上去找答案了, 不會(huì)找到有價(jià)值的東西。至少對(duì)我們這個(gè)論題來說是如此。我們還是繼續(xù)下一步討論吧。我們答案就在那里。
為什么是XML
有時(shí)候正確的決策并非完全出于深思熟慮。我不知道James選擇XML是否出于深思熟慮。也許僅僅是個(gè)下意識(shí)的決定。至少從James在Ant網(wǎng)站上發(fā)表的文章看起來, 他所說的理由完全是似是而非。他的主要理由是移植性和擴(kuò)展性, 在Ant案例上, 我看不出這兩條有什么幫助。使用XML而不是Java代碼, 到底有什么好處? 為什么不寫一組Java類, 提供api來滿足基本任務(wù)(拷貝目錄, 編譯等等), 然后在Java里直接調(diào)用這些代碼? 這樣做仍然可以保證移植性, 擴(kuò)展性也是毫無疑問的。而且語法也更為熟悉, 看著順眼。那為什么要用 XML呢? 有什么更好的理由嗎?
有的。雖然我不確定James是否確實(shí)意識(shí)到了。在語義的可構(gòu)造性方面, XML的彈性是Java望塵莫及的。我不想用高深莫測(cè)的名詞來嚇唬你, 其中的道理相當(dāng)簡(jiǎn)單, 解釋起來并不費(fèi)很多功夫。好, 做好預(yù)備動(dòng)作, 我們馬上就要朝向頓悟的時(shí)刻做奮力一躍。
上面的那個(gè)copy的例子, 用Java代碼怎樣實(shí)現(xiàn)呢? 我們可以這樣做:
- CopyTask copy = new CopyTask();
- Fileset fileset = new Fileset();
- fileset.setDir("src_dir");
- copy.setToDir("../new/dir");
- copy.setFileset(fileset);
- copy.excute();
這個(gè)代碼看起來和XML的那個(gè)很相似, 只是稍微長(zhǎng)一點(diǎn)。差別在那里? 差別在于XML構(gòu)造了一個(gè)特殊的copy動(dòng)詞, 如果我們硬要用Java來寫的話, 應(yīng)該是這個(gè)樣子:
- copy("../new/dir");
- {
- fileset("src_dir");
- }
看到差別了嗎? 以上代碼(如果可以在Java中用的化), 是一個(gè)特殊的copy算符, 有點(diǎn)像for循環(huán)或者Java5中的foreach循環(huán)。如果我們有一個(gè)轉(zhuǎn)換器, 可以把XML轉(zhuǎn)換到Java, 大概就會(huì)得到上面這段事實(shí)上不可以執(zhí)行的代碼。因?yàn)镴ava的技術(shù)規(guī)范是定死的, 我們沒有辦法在程序里改變它。我們可以增加包, 增加類, 增加方法, 但是我們沒辦法增加算符,而對(duì)于XML, 我們顯然可以任由自己增加這樣的東西。對(duì)于XML的語法樹來說, 只要原意,我們可以任意增加任何元素, 因此等于我們可以任意增加算符。如果你還不太明白的話,看下面這個(gè)例子, 加入我們要給Java引入一個(gè)unless算符:
- unless(someObject.canFly())
- {
- someObject.transportByGround():
- }
在上面的兩個(gè)例子中, 我們打算給Java語法擴(kuò)展兩個(gè)算符, 成組拷貝文件算符和條件算符unless, 我們要想做到這一點(diǎn), 就必須修改Java編譯器能夠接受的抽象語法樹, 顯然我們無法用Java標(biāo)準(zhǔn)的功能來實(shí)現(xiàn)它。但是在XML中我們可以輕而易舉地做到。我們的解析器根據(jù) XML元素, 生成抽象語法樹, 由此生成算符, 所以, 我們可以任意引入任何算符。
對(duì)于復(fù)雜的算符來說, 這樣做的好處顯而易見。比如, 用特定的算符來做檢出源碼, 編譯文件, 單元測(cè)試, 發(fā)送郵件等任務(wù), 想想看有多么美妙。對(duì)于特定的題目, 比如說構(gòu)造軟件項(xiàng)目, 這些算符的使用可以大幅減低少代碼的數(shù)量。增加代碼的清晰程度和可重用性。解釋性的XML可以很容易的達(dá)到這個(gè)目標(biāo)。XML是存儲(chǔ)層次化數(shù)據(jù)的簡(jiǎn)單數(shù)據(jù)文件, 而在Java中, 由于層次結(jié)構(gòu)是定死的(你很快就會(huì)看到, Lisp的情況與此截然不同), 我們就沒法達(dá)到上述目標(biāo)。也許這正是Ant的成功之處呢。
你可以注意一下最近Java和C#的變化(尤其是C#3.0的技術(shù)規(guī)范), C#把常用的功能抽象出來, 作為算符增加到C#中。C#新增加的query算符就是一個(gè)例子。它用的還是傳統(tǒng)的作法:C#的設(shè)計(jì)者修改抽象語法樹, 然后增加對(duì)應(yīng)的實(shí)現(xiàn)。如果程序員自己也能修改抽象語法樹該有多好! 那樣我們就可以構(gòu)造用于特定問題的子語言(比如說就像Ant這種用于構(gòu)造項(xiàng)目的語言), 你能想到別的例子嗎? 再思考一下這個(gè)概念。不過也不必思考太甚, 我們待會(huì)還會(huì)回到這個(gè)題目。那時(shí)候就會(huì)更加清晰。
#p#
離Lisp越來越近
我們先把算符的事情放一放, 考慮一下Ant設(shè)計(jì)局限之外的東西。我早先說過, Ant可以通過寫Java類來擴(kuò)展。Ant解析器會(huì)根據(jù)名字來匹配XML元素和Java類, 一旦找到匹配, 就執(zhí)行相應(yīng)任務(wù)。為什么不用Ant自己來擴(kuò)展Ant呢? 畢竟核心任務(wù)要包含很多傳統(tǒng)語言的結(jié)構(gòu)(例如”if”), 如果Ant自身就能提供構(gòu)造任務(wù)的能力(而不是依賴java類), 我們就可以得到更高的移植性。我們將會(huì)依賴一組核心任務(wù)(如果你原意, 也不妨把它稱作標(biāo)準(zhǔn)庫), 而不用管有沒有Java 環(huán)境了。這組核心任務(wù)可以用任何方式來實(shí)現(xiàn), 而其他任務(wù)建筑在這組核心任務(wù)之上, 那樣的話, Ant就會(huì)成為通用的, 可擴(kuò)展的, 基于XML的編程語言??紤]下面這種代碼的可能性:
- <task name="Test">
- <echo message="Hello World" />
- </task>
- <Test />
如果XML支持”task”的創(chuàng)建, 上面這段代碼就會(huì)輸出”Hello World!”. 實(shí)際上, 我們可以用Java寫個(gè)”task”任務(wù), 然后用Ant-XML來擴(kuò)展它。Ant可以在簡(jiǎn)單原語的基礎(chǔ)上寫出更復(fù)雜的原語, 就像其他編程語言常用的作法一樣。這也就是我們一開始提到的基于XML的編程語言。這樣做用處不大(你知道為甚么嗎?), 但是真的很酷。
再看一回我們剛才說的Task任務(wù)。祝賀你呀, 你在看Lisp代碼!!! 我說什么? 一點(diǎn)都不像Lisp嗎? 沒關(guān)系, 我們?cè)俳o它收拾一下。
比XML更好
前面一節(jié)說過, Ant自我擴(kuò)展沒什么大用, 原因在于XML很煩瑣。對(duì)于數(shù)據(jù)來說, 這個(gè)問題還不太大, 但如果代碼很煩瑣的話, 光是打字上的麻煩就足以抵消它的好處。你寫過Ant的腳本嗎? 我寫過, 當(dāng)腳本達(dá)到一定復(fù)雜度的時(shí)候, XML非常讓人厭煩。想想看吧, 為了寫結(jié)束標(biāo)簽, 每個(gè)詞都得打兩遍, 不發(fā)瘋算好的!
為了解決這個(gè)問題, 我們應(yīng)當(dāng)簡(jiǎn)化寫法。須知, XML僅僅是一種表達(dá)層次化數(shù)據(jù)的方式。我們并不是一定要使用尖括號(hào)才能得到樹的序列化結(jié)果。我們完全可以采用其他的格式。其中的一種(剛好就是Lisp 所采用的)格式, 叫做s表達(dá)式。s表達(dá)式要做的和XML一樣, 但它的好處是寫法更簡(jiǎn)單, 簡(jiǎn)單的寫法更適合代碼輸入。后面我會(huì)詳細(xì)講s表達(dá)式。這之前我要清理一下XML的東西。考慮一下關(guān)于拷貝文件的例子:
- <copy toDir="../new/dir">
- <fileset dir="src_dir">
- </copy>
想想看在內(nèi)存里面, 這段代碼的解析樹在內(nèi)存會(huì)是什么樣子? 會(huì)有一個(gè)”copy”節(jié)點(diǎn), 其下有一個(gè) “fileset”節(jié)點(diǎn), 但是屬性在哪里呢? 它怎樣表達(dá)呢? 如果你以前用過XML, 并且弄不清楚該用元素還是該用屬性, 你不用感到孤單, 別人一樣糊涂著呢。沒人真的搞得清楚。這個(gè)選擇與其說是基于技術(shù)的理由, 還不如說是閉著眼瞎摸。從概念上來講, 屬性也是一種元素, 任何屬性能做的, 元素一樣做得到。XML引入屬性的理由, 其實(shí)就是為了讓XML寫法不那么冗長(zhǎng)。比如我們看個(gè)例子:
- <copy>
- <toDir>../new/dir</toDir>
- <fileset>
- <dir>src_dir</dir>
- </fileset>
- </copy>
兩下比較, 內(nèi)容的信息量完全一樣, 用屬性可以減少打字?jǐn)?shù)量。如果XML沒有屬性的話,光是打字就夠把人搞瘋掉。
說完了屬性的問題, 我們?cè)賮砜匆豢磗表達(dá)式。之所以繞這么個(gè)彎, 是因?yàn)閟表達(dá)式?jīng)]有屬性的概念。因?yàn)閟表達(dá)式非常簡(jiǎn)練, 根本沒有必要引入屬性。我們?cè)诎裍ML轉(zhuǎn)換成s表達(dá)式的時(shí)候, 心里應(yīng)該記住這一點(diǎn)??磦€(gè)例子, 上面的代碼譯成s表達(dá)式是這樣的:
- (copy
- (todir "../new/dir")
- (fileset (dir "src_dir")))
仔細(xì)看看這個(gè)例子, 差別在哪里? 尖括號(hào)改成了圓括號(hào), 每個(gè)元素原來是有一對(duì)括號(hào)標(biāo)記包圍的, 現(xiàn)在取消了后一個(gè)(就是帶斜杠的那個(gè))括號(hào)標(biāo)記。表示元素的結(jié)束只需要一個(gè)”)”就可以了。不錯(cuò), 差別就是這些。這兩種表達(dá)方式的轉(zhuǎn)換, 非常自然, 也非常簡(jiǎn)單。s表達(dá)式打起字來, 也省事得多。***次看s表達(dá)式(Lisp)時(shí), 括號(hào)很煩人是吧? 現(xiàn)在我們明白了背后的道理, 一下子就變得容易多了。至少, 比XML要好的多。用s表達(dá)式寫代碼, 不單是實(shí)用, 而且也很讓人愉快。s表達(dá)式具有XML的一切好處, 這些好處是我們剛剛探討過的?,F(xiàn)在我們看看更加Lisp風(fēng)格的task例子:
- (task (name "Test")
- (echo (message "Hellow World!")))
- (Test)
用Lisp的行話來講, s表達(dá)式稱為表(list)。對(duì)于上面的例子, 如果我們寫的時(shí)候不加換行, 用逗號(hào)來代替空格, 那么這個(gè)表達(dá)式看起來就非常像一個(gè)元素列表, 其中又嵌套著其他標(biāo)記。
- (task, (name, "test"), (echo, (message, "Hello World!")))
XML自然也可以用這樣的風(fēng)格來寫。當(dāng)然上面這句并不是一般意義上的元素表。它實(shí)際上是一個(gè)樹。這和XML的作用是一樣的。稱它為列表, 希望你不會(huì)感到迷惑, 因?yàn)榍短妆砗蜆鋵?shí)際上是一碼事。Lisp的字面意思就是表處理(list processing), 其實(shí)也可以稱為樹處理, 這和處理XML節(jié)點(diǎn)沒有什么不同。
經(jīng)受這一番折磨以后, 現(xiàn)在我們終于相當(dāng)接近Lisp了, Lisp的括號(hào)的神秘本質(zhì)(就像許多Lisp狂熱分子認(rèn)為的)逐漸顯現(xiàn)出來?,F(xiàn)在我們繼續(xù)研究其他內(nèi)容。
#p#
重新審視C語言的宏
到了這里, 對(duì)XML的討論你大概都聽累了, 我都講累了。我們先停一停, 把樹, s表達(dá)式,Ant這些東西先放一放, 我們來說說C的預(yù)處理器。一定有人問了, 我們的話題和C有什么關(guān)系? 我們已經(jīng)知道了很多關(guān)于元編程的事情, 也探討過專門寫代碼的代碼。理解這問題有一定難度, 因?yàn)橄嚓P(guān)討論文章所使用的編程語言, 都是你們不熟悉的。但是如果只論概念的話, 就相對(duì)要簡(jiǎn)單一些。我相信, 如果以C語言做例子來討論元編程, 理解起來一定會(huì)容易得多。好, 我們接著看。
一個(gè)問題是, 為什么要用代碼來寫代碼呢? 在實(shí)際的編程中, 怎樣做到這一點(diǎn)呢? 到底元編程是什么意思? 你大概已經(jīng)聽說過這些問題的答案, 但是并不懂得其中緣由。為了揭示背后的真理, 我們來看一下一個(gè)簡(jiǎn)單的數(shù)據(jù)庫查詢問題。這種題目我們都做過。比方說,直接在程序碼里到處寫SQL語句來修改表(table)里的數(shù)據(jù), 寫多了就非常煩人。即便用C#3.0的LINQ, 仍然不減其痛苦。寫一個(gè)完整的SQL查詢(盡管語法很優(yōu)美)來修改某人的地址, 或者查找某人的名字, 絕對(duì)是件令程序員倍感乏味的事情, 那么我們?cè)撛鯓觼斫鉀Q這個(gè)問題? 答案就是: 使用數(shù)據(jù)訪問層。
概念挺簡(jiǎn)單, 其要點(diǎn)是把數(shù)據(jù)訪問的內(nèi)容(至少是那些比較瑣碎的部分)抽象出來, 用類來映射數(shù)據(jù)庫的表, 然后用訪問對(duì)象屬性訪問器(accessor)的辦法來間接實(shí)現(xiàn)查詢。這樣就極大地簡(jiǎn)化了開發(fā)工作量。我們用訪問對(duì)象的方法(或者屬性賦值, 這要視你選用的語言而定)來代替寫SQL查詢語句。凡是用過這種方法的人, 都知道這很節(jié)省時(shí)間。當(dāng)然, 如果你要親自寫這樣一個(gè)抽象層, 那可是要花非常多的時(shí)間的–你要寫一組類來映射表, 把屬性訪問轉(zhuǎn)換為SQL查詢, 這個(gè)活相當(dāng)耗費(fèi)精力。用手工來做顯然是很不明智的。但是一旦你有了方案和模板, 實(shí)際上就沒有多少東西需要思考的。你只需要按照同樣的模板一次又一次重復(fù)編寫相似代碼就可以了。事實(shí)上很多人已經(jīng)發(fā)現(xiàn)了更好的方法, 有一些工具可以幫助你連接數(shù)據(jù)庫, 抓取數(shù)據(jù)庫結(jié)構(gòu)定義(schema), 按照預(yù)定義的或者用戶定制的模板來自動(dòng)編寫代碼。
如果你用過這種工具, 你肯定會(huì)對(duì)它的神奇效果深為折服。往往只需要鼠標(biāo)點(diǎn)擊數(shù)次, 就可以連接到數(shù)據(jù)庫, 產(chǎn)生數(shù)據(jù)訪問源碼, 然后把文件加入到你的工程里面, 十幾分鐘的工作, 按照往常手工方式來作的話, 也許需要數(shù)百個(gè)小時(shí)人工(man-hours)才能完成??墒?如果你的數(shù)據(jù)庫結(jié)構(gòu)定義后來改變了怎么辦? 那樣的話, 你只需把這個(gè)過程重復(fù)一遍就可以了。甚至有一些工具能自動(dòng)完成這項(xiàng)變動(dòng)工作。你只要把它作為工程構(gòu)造的一部分, 每次編譯工程的時(shí)候, 數(shù)據(jù)庫部分也會(huì)自動(dòng)地重新構(gòu)造。這真的太棒了。你要做的事情基本上減到了0。如果數(shù)據(jù)庫結(jié)構(gòu)定義發(fā)生了改變, 并在編譯時(shí)自動(dòng)更新了數(shù)據(jù)訪問層的代碼,那么程序中任何使用過時(shí)的舊代碼的地方, 都會(huì)引發(fā)編譯錯(cuò)誤。
數(shù)據(jù)訪問層是個(gè)很好的例子, 這樣的例子還有好多。從GUI樣板代碼, WEB代碼, COM和CORBA存根, 以及MFC和ATL等等。在這些地方, 都是有好多相似代碼多次重復(fù)。既然這些代碼有可能自動(dòng)編寫, 而程序員時(shí)間又遠(yuǎn)遠(yuǎn)比CPU時(shí)間昂貴, 當(dāng)然就產(chǎn)生了好多工具來自動(dòng)生成樣板代碼。這些工具的本質(zhì)是什么呢? 它們實(shí)際上就是制造程序的程序。它們有一個(gè)神秘的名字, 叫做元編程。所謂元編程的本義, 就是如此。
元編程本來可以用到無數(shù)多的地方, 但實(shí)際上使用的次數(shù)卻沒有那么多。歸根結(jié)底, 我們心里還是在盤算, 假設(shè)重復(fù)代碼用拷貝粘貼的話, 大概要重復(fù)6,7次, 對(duì)于這樣的工作量,值得專門建立一套生成工具嗎? 當(dāng)然不值得。數(shù)據(jù)訪問層和COM存根往往需要重用數(shù)百次,甚至上千次, 所以用工具生成是***的辦法。而那些僅僅是重復(fù)幾次十幾次的代碼, 是沒有必要專門做工具的。不必要的時(shí)候也去開發(fā)代碼生成工具, 那就顯然過度估計(jì)了代碼生成的好處。當(dāng)然, 如果創(chuàng)建這類工具足夠簡(jiǎn)單的話, 還是應(yīng)當(dāng)盡量多用, 因?yàn)檫@樣做必然會(huì)節(jié)省時(shí)間?,F(xiàn)在來看一下有沒有合理的辦法來達(dá)到這個(gè)目的。
現(xiàn)在, C預(yù)處理器要派上用場(chǎng)了。我們都用過C/C++的預(yù)處理器, 我們用它執(zhí)行簡(jiǎn)單的編譯指令, 來產(chǎn)生簡(jiǎn)單的代碼變換(比方說, 設(shè)置調(diào)試代碼開關(guān)), 看一個(gè)例子:
- #define triple(X) X+X+X
這一行的作用是什么? 這是一個(gè)簡(jiǎn)單的預(yù)編譯指令, 它把程序中的triple(X)替換稱為X+X+X。例如, 把所有的triple(5)都換成5+5+5, 然后再交給編譯器編譯。這就是一個(gè)簡(jiǎn)單的代碼生成的例子。要是C的預(yù)處理器再強(qiáng)大一點(diǎn), 要是能夠允許連接數(shù)據(jù)庫, 要是能多一些其他簡(jiǎn)單的機(jī)制, 我們就可以在我們程序的內(nèi)部開發(fā)自己的數(shù)據(jù)訪問層。下面這個(gè)例子, 是一個(gè)假想的對(duì)C宏的擴(kuò)展:
- #get-db-schema("127.0.0.1")
- #iterate-through-tables
- #for-each-table
- class #table-name
- {
- };
- #end-for-each
我們連接數(shù)據(jù)庫結(jié)構(gòu)定義, 遍歷數(shù)據(jù)表, 然后對(duì)每個(gè)表創(chuàng)建一個(gè)類, 只消幾行代碼就完成了這個(gè)工作。這樣每次編譯工程的時(shí)候, 這些類都會(huì)根據(jù)數(shù)據(jù)庫的定義同步更新。顯而易見, 我們不費(fèi)吹灰之力就在程序內(nèi)部建立了一個(gè)完整的數(shù)據(jù)訪問層, 根本用不著任何外部工具。當(dāng)然這種作法有一個(gè)缺點(diǎn), 那就是我們得學(xué)習(xí)一套新的”編譯時(shí)語言”, 另一個(gè)缺點(diǎn)就是根本不存在這么一個(gè)高級(jí)版的C預(yù)處理器。需要做復(fù)雜代碼生成的時(shí)候, 這個(gè)語言(譯者注: 這里指預(yù)處理指令, 即作者所說的”編譯時(shí)語言”)本身也一定會(huì)變得相當(dāng)復(fù)雜。它必須支持足夠多的庫和語言結(jié)構(gòu)。比如說我們想要生成的代碼要依賴某些ftp服務(wù)器上的文件, 預(yù)處理器就得支持ftp訪問, 僅僅因?yàn)檫@個(gè)任務(wù)而不得不創(chuàng)造和學(xué)習(xí)一門新的語言,真是有點(diǎn)讓人惡心(事實(shí)上已經(jīng)存在著有此能力的語言, 這樣做就更顯荒謬)。我們不妨再靈活一點(diǎn), 為什么不直接用 C/C++自己作為自己的預(yù)處理語言呢? 這樣子的話, 我們可以發(fā)揮語言的強(qiáng)大能力, 要學(xué)的新東西也只不過是幾個(gè)簡(jiǎn)單的指示字 , 這些指示字用來區(qū)別編譯時(shí)代碼和運(yùn)行時(shí)代碼。
- <%
- cout<<"Enter a number: ";
- cin>>n;
- %>
- for(int i=0;i< <% n %>;i++)
- {
- cout<<"hello"<<endl;
- }
你明白了嗎? 在<%和%>標(biāo)記之間的代碼是在編譯時(shí)運(yùn)行的, 標(biāo)記之外的其他代碼都是普通代碼。編譯程序時(shí), 系統(tǒng)會(huì)提示你輸入一個(gè)數(shù), 這個(gè)數(shù)在后面的循環(huán)中會(huì)用到。而for循環(huán)的代碼會(huì)被編譯。假定你在編譯時(shí)輸入5, for循環(huán)的代碼將會(huì)是:
- for(int i=0;i<5; i++)
- {
- cout<<"hello"<<endl;
- }
又簡(jiǎn)單又有效率, 也不需要另外的預(yù)處理語言。我們可以在編譯時(shí)就充分發(fā)揮宿主語言(此處是C/C++)的強(qiáng)大能力, 我們可以很容易地在編譯時(shí)連接數(shù)據(jù)庫, 建立數(shù)據(jù)訪問層, 就像JSP或者ASP創(chuàng)建網(wǎng)頁那樣。我們也用不著專門的窗口工具來另外建立工程。我們可以在代碼中立即加入必要的工具。我們也用不著顧慮建立這種工具是不是值得, 因?yàn)檫@太容易了, 太簡(jiǎn)單了。這樣子不知可以節(jié)省多少時(shí)間啊。
#p#
你好, Lisp
到此刻為止, 我們所知的關(guān)于Lisp的指示可以總結(jié)為一句話: Lisp是一個(gè)可執(zhí)行的語法更優(yōu)美的XML, 但我們還沒有說Lisp是怎樣做到這一點(diǎn)的, 現(xiàn)在開始補(bǔ)上這個(gè)話題。
Lisp有豐富的內(nèi)置數(shù)據(jù)類型, 其中的整數(shù)和字符串和其他語言沒什么分別。像71或者”hello”這樣的值, 含義也和C++或者Java這樣的語言大體相同。真正有意思的三種類型是符號(hào)(symbol), 表和函數(shù)。這一章的剩余部分, 我都會(huì)用來介紹這幾種類型, 還要介紹Lisp環(huán)境是怎樣編譯和運(yùn)行源碼的。這個(gè)過程用Lisp的術(shù)語來說通常叫做求值。通讀這一節(jié)內(nèi)容, 對(duì)于透徹理解元編程的真正潛力, 以及代碼和數(shù)據(jù)的同一性, 和面向領(lǐng)域語言的觀念, 都極其重要。萬勿等閑視之。我會(huì)盡量講得生動(dòng)有趣一些, 也希望你能獲得一些啟發(fā)。那好, 我們先講符號(hào)。
大體上, 符號(hào)相當(dāng)于C++或Java語言中的標(biāo)志符, 它的名字可以用來訪問變量值(例如currentTime, arrayCount, n, 等等), 差別在于, Lisp中的符號(hào)更加基本。在C++或Java里面, 變量名只能用字母和下劃線的組合, 而Lisp的符號(hào)則非常有包容性, 比如, 加號(hào)(+)就是一個(gè)合法的符號(hào), 其他的像-, =, hello-world, *等等都可以是符號(hào)名。符號(hào)名的命名規(guī)則可以在網(wǎng)上查到。你可以給這些符號(hào)任意賦值, 我們這里先用偽碼來說明這一點(diǎn)。假定函數(shù)set是給變量賦值(就像等號(hào)=在C++和Java里的作用), 下面是我們的例子:
- set(test, 5) // 符號(hào)test的值為5
- set(=, 5) // 符號(hào)=的值為5
- set(test, "hello") // 符號(hào)test的值為字符串"hello"
- set(test, =) // 此時(shí)符號(hào)=的值為5, 所以test的也為5
- set(*, "hello") // 符號(hào)*的值為"hello"
好像有什么不對(duì)的地方? 假定我們對(duì)*賦給整數(shù)或者字符串值, 那做乘法時(shí)怎么辦? 不管怎么說, *總是乘法呀? 答案簡(jiǎn)單極了。Lisp中函數(shù)的角色十分特殊, 函數(shù)也是一種數(shù)據(jù)類型, 就像整數(shù)和字符串一樣, 因此可以把它賦值給符號(hào)。乘法函數(shù)Lisp的內(nèi)置函數(shù), 默認(rèn)賦給*, 你可以把其他函數(shù)賦值給*, 那樣*就不代表乘法了。你也可以把這函數(shù)的值存到另外的變量里。我們?cè)儆脗未a來說明一下:
- 3,4) // 3乘4, 結(jié)果是12
- set(temp, *) // 把*的值, 也就是乘法函數(shù), 賦值給temp
- set(*, 3) // 把3賦予*
- *(3,4) // 錯(cuò)誤的表達(dá)式, *不再是乘法, 而是數(shù)值3
- temp(3,4) // temp是乘法函數(shù), 所以此表達(dá)式的值為3乘4等于12
- set(*, temp) // 再次把乘法函數(shù)賦予*
- *(3,4) // 3乘4等于12
再古怪一點(diǎn), 把減號(hào)的值賦給加號(hào):
- set(+, -) // 減號(hào)(-)是內(nèi)置的減法函數(shù)
- +(5, 4) // 加號(hào)(+)現(xiàn)在是代表減法函數(shù), 結(jié)果是5減4等于1
這只是舉例子, 我還沒有詳細(xì)講函數(shù)。Lisp中的函數(shù)是一種數(shù)據(jù)類型, 和整數(shù), 字符串,符號(hào)等等一樣。一個(gè)函數(shù)并不必然有一個(gè)名字, 這和C++或者Java語言的情形很不相同。在這里函數(shù)自己代表自己。事實(shí)上它是一個(gè)指向代碼塊的指針, 附帶有一些其他信息(例如一組參數(shù)變量)。只有在把函數(shù)賦予其他符號(hào)時(shí), 它才具有了名字, 就像把一個(gè)數(shù)值或字符串賦予變量一樣的道理。你可以用一個(gè)內(nèi)置的專門用于創(chuàng)建函數(shù)的函數(shù)來創(chuàng)建函數(shù),然后把它賦值給符號(hào)fn, 用偽碼來表示就是:
- fn [a]
- {
- return *(a, 2);
- }
這段代碼返回一個(gè)具有一個(gè)參數(shù)的函數(shù), 函數(shù)的功能是計(jì)算參數(shù)乘2的結(jié)果。這個(gè)函數(shù)還沒有名字, 你可以把此函數(shù)賦值給別的符號(hào):
- set(times-two, fn [a] {return *(a, 2)})
我們現(xiàn)在可以這樣調(diào)用這個(gè)函數(shù):
- time-two(5) // 返回10
我們先跳過符號(hào)和函數(shù), 講一講表。什么是表? 你也許已經(jīng)聽過好多相關(guān)的說法。表, 一言以蔽之, 就是把類似XML那樣的數(shù)據(jù)塊, 用s表達(dá)式來表示。表用一對(duì)括號(hào)括住, 表中元素以空格分隔, 表可以嵌套。例如(這回我們用真正的Lisp語法, 注意用分號(hào)表示注釋):
- () ; 空表
- (1) ; 含一個(gè)元素的表
- (1 "test") ; 兩元素表, 一個(gè)元素是整數(shù)1, 另一個(gè)是字符串
- (test "hello") ; 兩元素表, 一個(gè)元素是符號(hào), 另一個(gè)是字符串
- (test (1 2) "hello") ; 三元素表, 一個(gè)符號(hào)test, 一個(gè)含有兩個(gè)元素1和2的
- ; 表, ***一個(gè)元素是字符串
- (* 3 4) ; 相當(dāng)于前面列舉過的偽碼*(3,4), 即計(jì)算3乘4
- (times-two 5) ; 返回10, times-two按照前面的定義是求參數(shù)的2倍
- (3 4) ; 錯(cuò)誤, 3不是函數(shù)
- (time-two) ; 錯(cuò)誤, times-two要求一個(gè)參數(shù)
- (times-two 3 4) ; 錯(cuò)誤, times-two只要求一個(gè)參數(shù)
- (set + -) ; 把減法函數(shù)賦予符號(hào)+
- (+ 5 4) ; 依據(jù)上一句的結(jié)果, 此時(shí)+表示減法, 所以返回1
- (* 3 (+ 2 2)) ; 2+2的結(jié)果是4, 再乘3, 結(jié)果是12
- (set test '(1 2)) ; test的值為兩元素表
- (set test (1 2)) ; 錯(cuò)誤, 1不是函數(shù)
- (set test '(* 3 4)) ; test的值是三元素表, 三個(gè)元素分別是*, 3, 4
- (head '(* 3 4)) ; 返回符號(hào)*
- (tail '(* 3 4)) ; 返回表(3 4)
- (head (tal '(* 3 4))) ; 返回3
- (head test) ; 返回*
你可以把Lisp的內(nèi)置函數(shù)想像成Ant的任務(wù)。差別在于, 我們不用在另外的語言中擴(kuò)展Lisp(雖然完全可以做得到), 我們可以用Lisp自己來擴(kuò)展自己, 就像上面舉的times-two函數(shù)的例子。Lisp的內(nèi)置函數(shù)集十分精簡(jiǎn), 只包含了十分必要的部分。剩下的函數(shù)都是作為標(biāo)準(zhǔn)庫來實(shí)現(xiàn)的。
#p#
Lisp宏
我們已經(jīng)看到, 元編程在一個(gè)類似jsp的模板引擎方面的應(yīng)用。我們通過簡(jiǎn)單的字符串處理來生成代碼。但是我們可以做的更好。我們先提一個(gè)問題, 怎樣寫一個(gè)工具, 通過查找目錄結(jié)構(gòu)中的源文件來自動(dòng)生成Ant腳本。
用字符串處理的方式生成Ant腳本是一種簡(jiǎn)單的方式。當(dāng)然, 還有一種更加抽象, 表達(dá)能力更強(qiáng), 擴(kuò)展性更好的方式, 就是利用XML庫在內(nèi)存中直接生成XML節(jié)點(diǎn), 這樣的話內(nèi)存中的節(jié)點(diǎn)就可以自動(dòng)序列化成為字符串。不僅如此, 我們的工具還可以分析這些節(jié)點(diǎn), 對(duì)已有的XML文件做變換。通過直接處理XML節(jié)點(diǎn)。我們可以超越字符串處理, 使用更高層次的概念, 因此我們的工作就會(huì)做的更快更好。
我們當(dāng)然可以直接用Ant自身來處理XML變換和制作代碼生成工具?;蛘呶覀円部梢杂肔isp來做這項(xiàng)工作。正像我們以前所知的, 表是Lisp內(nèi)置的數(shù)據(jù)結(jié)構(gòu), Lisp含有大量的工具來快速有效的操作表(head和tail是最簡(jiǎn)單的兩個(gè))。而且, Lisp沒有語義約束, 你可以構(gòu)造任何數(shù)據(jù)結(jié)構(gòu), 只要你原意。
Lisp通過宏(macro)來做元編程。我們寫一組宏來把任務(wù)列表(to-do list)轉(zhuǎn)換為專用領(lǐng)域語言。
回想一下上面to-do list的例子, 其XML的數(shù)據(jù)格式是這樣的:
- <todo name = "housework">
- <item priority = "high">Clean the hose</item>
- <item priority = "medium">Wash the dishes</item>
- <item priority = "medium">Buy more soap</item>
- </todo>
相應(yīng)的s表達(dá)式是這樣的:
- (todo "housework"
- (item (priority high) "Clean the house")
- (item (priority medium) "Wash the dishes")
- (item (priority medium) "Buy more soap"))
假設(shè)我們要寫一個(gè)任務(wù)表的管理程序, 把任務(wù)表數(shù)據(jù)存到一組文件里, 當(dāng)程序啟動(dòng)時(shí), 從文件讀取這些數(shù)據(jù)并顯示給用戶。在別的語言里(比如說Java), 這個(gè)任務(wù)該怎么做? 我們會(huì)解析XML文件, 從中得出任務(wù)表數(shù)據(jù), 然后寫代碼遍歷XML樹, 再轉(zhuǎn)換為Java的數(shù)據(jù)結(jié)構(gòu)(老實(shí)講, 在Java里解析XML真不是件輕松的事情), ***再把數(shù)據(jù)展示給用戶?,F(xiàn)在如果用Lisp, 該怎么做?
假定要用同樣思路的化, 我們大概會(huì)用Lisp庫來解析XML。XML對(duì)我們來說就是一個(gè)Lisp的表(s表達(dá)式), 我們可以遍歷這個(gè)表, 然后把相關(guān)數(shù)據(jù)提交給用戶??墒? 既然我們用Lisp, 就根本沒有必要再用XML格式保存數(shù)據(jù), 直接用s表達(dá)式就好了, 這樣就沒有必要做轉(zhuǎn)換了。我們也用不著專門的解析庫, Lisp可以直接在內(nèi)存里處理s表達(dá)式。注意, Lisp編譯器和.net編譯器一樣, 對(duì)Lisp程序來說, 在運(yùn)行時(shí)總是隨時(shí)可用的。
但是還有更好的辦法。我們甚至不用寫表達(dá)式來存儲(chǔ)數(shù)據(jù), 我們可以寫宏, 把數(shù)據(jù)當(dāng)作代碼來處理。那該怎么做呢? 真的簡(jiǎn)單?;叵胍幌? Lisp的函數(shù)調(diào)用格式:
- (function-name arg1 arg2 arg3)
其中每個(gè)參數(shù)都是s表達(dá)式, 求值以后, 傳遞給函數(shù)。如果我們用(+ 4 5)來代替arg1,那么, 程序會(huì)先求出結(jié)果, 就是9, 然后把9傳遞給函數(shù)。宏的工作方式和函數(shù)類似。主要的差別是, 宏的參數(shù)在代入時(shí)不求值。
- (macro-name (+ 4 5))
這里, (+ 4 5)作為一個(gè)表傳遞給宏, 然后宏就可以任意處理這個(gè)表, 當(dāng)然也可以對(duì)它求值。宏的返回值是一個(gè)表, 然后有程序作為代碼來執(zhí)行。宏所占的位置, 就被替換為這個(gè)結(jié)果代碼。我們可以定義一個(gè)宏把數(shù)據(jù)替換為任意代碼, 比方說, 替換為顯示數(shù)據(jù)給用戶的代碼。
這和元編程, 以及我們要做的任務(wù)表程序有什么關(guān)系呢? 實(shí)際上, 編譯器會(huì)替我們工作,調(diào)用相應(yīng)的宏。我們所要做的, 僅僅是創(chuàng)建一個(gè)把數(shù)據(jù)轉(zhuǎn)換為適當(dāng)代碼的宏。
例如, 上面曾經(jīng)將過的C的求三次方的宏, 用Lisp來寫是這樣子:
- (defmacro triple (x)
- `(+ ~x ~x ~x))
(譯注: 在Common Lisp中, 此處的單引號(hào)應(yīng)當(dāng)是反單引號(hào), 意思是對(duì)表不求值, 但可以對(duì)表中某元素求值, 記號(hào)~表示對(duì)元素x求值, 這個(gè)求值記號(hào)在Common Lisp中應(yīng)當(dāng)是逗號(hào)。反單引號(hào)和單引號(hào)的區(qū)別是, 單引號(hào)標(biāo)識(shí)的表, 其中的元素都不求值。這里作者所用的記號(hào)是自己發(fā)明的一種Lisp方言Blaise, 和common lisp略有不同, 事實(shí)上, 發(fā)明方言是lisp高手獨(dú)有的樂趣, 很多狂熱分子都熱衷這樣做。比如Paul Graham就發(fā)明了ARC, 許多記號(hào)比傳統(tǒng)的Lisp簡(jiǎn)潔得多, 顯得比較現(xiàn)代)
單引號(hào)的用處是禁止對(duì)表求值。每次程序中出現(xiàn)triple的時(shí)候,
- (triple 4)
都會(huì)被替換成:
- (+ 4 4 4)
我們可以為任務(wù)表程序?qū)懸粋€(gè)宏, 把任務(wù)數(shù)據(jù)轉(zhuǎn)換為可執(zhí)行碼, 然后執(zhí)行。假定我們的輸出是在控制臺(tái):
- (defmacro item (priority note)
- `(block
- (print stdout tab "Prority: " ~(head (tail priority)) endl)
- (print stdout tab "Note: " ~note endl endl)))
我們創(chuàng)造了一個(gè)非常小的有限的語言來管理嵌在Lisp中的任務(wù)表。這個(gè)語言只用來解決特定領(lǐng)域的問題, 通常稱之為DSLs(特定領(lǐng)域語言, 或?qū)S妙I(lǐng)域語言)。
#p#
特定領(lǐng)域語言
本文談到了兩個(gè)特定領(lǐng)域語言, 一個(gè)是Ant, 處理軟件構(gòu)造。一個(gè)是沒起名字的, 用于處理任務(wù)表。兩者的差別在于, Ant是用XML, XML解析器, 以及Java語言合在一起構(gòu)造出來的。而我們的迷你語言則完全內(nèi)嵌在Lisp中, 只消幾分鐘就做出來了。
我們已經(jīng)說過了DSL的好處, 這也就是Ant用XML而不直接用Java的原因。如果使用Lisp,我們可以任意創(chuàng)建DSL, 只要我們需要。我們可以創(chuàng)建用于網(wǎng)站程序的DSL, 可以寫多用戶游戲, 做固定收益貿(mào)易(fixed income trade), 解決蛋白質(zhì)折疊問題, 處理事務(wù)問題, 等等。我們可以把這些疊放在一起, 造出一個(gè)語言, 專門解決基于網(wǎng)絡(luò)的貿(mào)易程序, 既有網(wǎng)絡(luò)語言的優(yōu)勢(shì), 又有貿(mào)易語言的好處。每天我們都會(huì)收獲這種方法帶給我們的益處, 遠(yuǎn)遠(yuǎn)超過Ant所能給予我們的。
用DSL解決問題, 做出的程序精簡(jiǎn), 易于維護(hù), 富有彈性。在Java里面, 我們可以用類來處理問題。這兩種方法的差別在于, Lisp使我們達(dá)到了一個(gè)更高層次的抽象, 我們不再受語言解析器本身的限制, 比較一下用Java庫直接寫的構(gòu)造腳本和用Ant寫的構(gòu)造腳本其間的差別。同樣的, 比較一下你以前所做的工作, 你就會(huì)明白Lisp帶來的好處。
接下來
學(xué)習(xí)Lisp就像戰(zhàn)爭(zhēng)中爭(zhēng)奪山頭。盡管在電腦科學(xué)領(lǐng)域, Lisp已經(jīng)算是一門古老的語言, 直到現(xiàn)在仍然很少有人真的明白該怎樣給初學(xué)者講授Lisp。盡管Lisp老手們盡了很大努力,今天新手學(xué)習(xí)Lisp仍然是困難重重。好在現(xiàn)在事情正在發(fā)生變化, Lisp的資源正在迅速增加, 隨著時(shí)間推移, Lisp將會(huì)越來越受關(guān)注。
Lisp使人超越平庸, 走到前沿。學(xué)會(huì)Lisp意味著你能找到更好的工作, 因?yàn)槁斆鞯墓椭鲿?huì)被你與眾不同的洞察力所打動(dòng)。學(xué)會(huì)Lisp也可能意味著明天你可能會(huì)被解雇, 因?yàn)槟憧偸菑?qiáng)調(diào), 如果公司所有軟件都用Lisp寫, 公司將會(huì)如何卓越, 而這些話你的同事會(huì)聽煩的。Lisp值得努力學(xué)習(xí)嗎? 那些已經(jīng)學(xué)會(huì)Lisp的人都說值得, 當(dāng)然, 這取決于你的判斷。
英文原文:The Nature of Lisp
譯文鏈接:Alpha(奧法)