說(shuō)明
函數(shù)式編程
和面向?qū)ο缶幊?/code>可以說(shuō)是編程的兩大宗教胧奔,猶如編輯器之爭(zhēng)一樣皇拣,之間口角不斷。我雖然靠著
OOP
的主力語(yǔ)言Java
上班掙錢養(yǎng)活自己逻翁,但受到《計(jì)算機(jī)程序的構(gòu)造和解釋》饥努、《黑客與畫家》的‘蠱惑’以后,對(duì)函數(shù)式編程的所謂完美世界很是心向往之八回。
最近發(fā)現(xiàn)一篇文章--《Lisp的本質(zhì)》,英文版《The Nature of Lisp》酷愧,非常棒,所以分成幾篇文章缠诅,轉(zhuǎn)過(guò)來(lái)溶浴。
以下為原文:
簡(jiǎn)介
最初在web的某些角落偶然看到有人贊美Lisp時(shí), 我那時(shí)已經(jīng)是一個(gè)頗有經(jīng)驗(yàn)的程序員。在我的履歷上, 掌握的語(yǔ)言范圍相當(dāng)廣泛, 象C++, Java, C#主流語(yǔ)言等等都不在話下, 我覺(jué)得我差不多知道所有的有關(guān)編程語(yǔ)言的事情管引。對(duì)待編程語(yǔ)言的問(wèn)題上, 我覺(jué)得自己不太會(huì)遇到什么大問(wèn)題士败。其實(shí)我大錯(cuò)特錯(cuò)了。
我試著學(xué)了一下Lisp, 結(jié)果馬上就撞了墻汉匙。我被那些范例代碼嚇壞了拱烁。我想很多初次接觸Lisp語(yǔ)言的人, 一定也有過(guò)類似的感受。Lisp的語(yǔ)法太次了噩翠。一個(gè)語(yǔ)言的發(fā)明人, 居然不肯用心弄出一套漂亮的語(yǔ)法, 那誰(shuí)還會(huì)愿意學(xué)它戏自。反正, 我是確確實(shí)實(shí)被那些難看的無(wú)數(shù)
的括號(hào)搞蒙了。
回過(guò)神來(lái)之后, 我和Lisp社區(qū)的那伙人交談, 訴說(shuō)我的沮喪心情伤锚。結(jié)果, 立馬就有一大套理論砸過(guò)來(lái), 這套理論在Lisp社區(qū)處處可見(jiàn), 幾成慣例擅笔。比如說(shuō): Lisp的括號(hào)只是表面現(xiàn)象; Lisp的代碼和數(shù)據(jù)的表達(dá)方式?jīng)]有差別, 而且比XML語(yǔ)法高明許多, 所以有無(wú)窮的好處; Lisp有強(qiáng)大無(wú)比的元語(yǔ)言能力, 程序員可以寫出自我維護(hù)的代碼; Lisp可以創(chuàng)造出針對(duì)特定應(yīng)用的語(yǔ)言子集; Lisp的運(yùn)行時(shí)和編譯時(shí)沒(méi)有明確的分界; 等等, 等等, 等等。這么長(zhǎng)的贊美詞雖然看起來(lái)相當(dāng)動(dòng)人, 不過(guò)對(duì)我毫無(wú)意義屯援。沒(méi)人能給我演示這些東西是如何應(yīng)用的, 因?yàn)檫@些東西一般來(lái)說(shuō)只有在大型系統(tǒng)才會(huì)用到猛们。我爭(zhēng)辯說(shuō), 這些東西傳統(tǒng)語(yǔ)言一樣辦得到。在和別人爭(zhēng)論了數(shù)個(gè)小時(shí)之后, 我最終還是放棄了學(xué)Lisp的念頭狞洋。為什么要花費(fèi)幾個(gè)月的時(shí)間學(xué)習(xí)語(yǔ)法這么難看的語(yǔ)言呢? 這種語(yǔ)言的概念這么晦澀, 又沒(méi)什么好懂的例子弯淘。也許這語(yǔ)言不是該我這樣的人學(xué)的。
幾個(gè)月來(lái), 我承受著這些Lisp辯護(hù)士對(duì)我心靈的重壓吉懊。我一度陷入了困惑庐橙。我認(rèn)識(shí)一些絕頂聰明的人, 我對(duì)他們相當(dāng)尊敬, 我看到他們對(duì)Lisp的贊美達(dá)到了宗教般的高度。這就是說(shuō), Lisp中一定有某種神秘的東西存在, 我不能忍受自己對(duì)此的無(wú)知, 好奇心和求知欲最
終不可遏制借嗽。我于是咬緊牙關(guān)埋頭學(xué)習(xí)Lisp, 經(jīng)過(guò)幾個(gè)月的時(shí)間費(fèi)勁心力的練習(xí), 終于,我看到了那無(wú)窮無(wú)盡的泉水的源頭态鳖。在經(jīng)過(guò)脫胎換骨的磨練之后, 在經(jīng)過(guò)七重地獄的煎熬之后, 終于, 我明白了。
頓悟在突然之間來(lái)臨恶导。曾經(jīng)許多次, 我聽(tīng)到別人引用雷蒙德(譯者注: 譯文<<大教堂和市集>>的作者, 著名的黑客社區(qū)理論家)的話: "Lisp語(yǔ)言值得學(xué)習(xí)浆竭。當(dāng)你學(xué)會(huì)Lisp之后, 你會(huì)擁有深刻的體驗(yàn)。就算你平常并不用Lisp編程, 它也會(huì)使你成為更加優(yōu)秀的程序員"惨寿。過(guò)去, 我根本不懂這些話的含義, 我也不相信這是真的邦泄。可是現(xiàn)在我懂得了缤沦。這些話蘊(yùn)含
的真理遠(yuǎn)遠(yuǎn)超過(guò)我過(guò)去的想像虎韵。我內(nèi)心體會(huì)到一種神圣的情感, 一瞬間的頓悟, 幾乎使我對(duì)電腦科學(xué)的觀念發(fā)生了根本的改變。
頓悟的那一刻, 我成了Lisp的崇拜者缸废。我體驗(yàn)到了宗教大師的感受: 一定要把我的知識(shí)傳布開(kāi)來(lái), 至少要讓10個(gè)迷失的靈魂得到拯救包蓝。按照通常的辦法, 我把這些道理(就是剛開(kāi)始別人砸過(guò)來(lái)的那一套, 不過(guò)現(xiàn)在我明白了真實(shí)的含義)告訴旁人。結(jié)果太令人失望了, 只有少數(shù)幾個(gè)人在我堅(jiān)持之下, 發(fā)生了一點(diǎn)興趣, 但是僅僅看了幾眼Lisp代碼, 他們就退卻了企量。照這樣的辦法, 也許費(fèi)數(shù)年功夫能造就了幾個(gè)Lisp迷, 但我覺(jué)得這樣的結(jié)果太差強(qiáng)人意了, 我得想一套有更好的辦法测萎。
我深入地思考了這個(gè)問(wèn)題。是不是Lisp有什么很艱深的東西, 令得那么多老練的程序員都不能領(lǐng)會(huì)? 不是, 沒(méi)有任何絕對(duì)艱深的東西届巩。因?yàn)槲夷芘? 我相信其他人也一定能硅瞧。那么問(wèn)題出在那里? 后來(lái)我終于找到了答案。我的結(jié)論就是, 凡是教人學(xué)高級(jí)概念, 一定要從他已經(jīng)懂得的東西開(kāi)始恕汇。如果學(xué)習(xí)過(guò)程很有趣, 學(xué)習(xí)的內(nèi)容表達(dá)得很恰當(dāng), 新概念就會(huì)變得相當(dāng)直觀腕唧。這就是我的答案或辖。所謂元編程, 所謂數(shù)據(jù)和代碼形式合一, 所謂自修改代碼, 所謂特定應(yīng)用的子語(yǔ)言, 所有這些概念根本就是同族概念, 彼此互為解釋, 肯定越講越不明白。還是從實(shí)際的例子出發(fā)最有用枣接。
我把我的想法說(shuō)給Lisp程序員聽(tīng), 遭到了他們的反對(duì)颂暇。"這些東西本身當(dāng)然不可能用熟悉的知識(shí)來(lái)解釋, 這些概念完全與眾不同, 你不可能在別人已有的經(jīng)驗(yàn)里找到類似的東西",可是我認(rèn)為這些都是遁詞。他們又反問(wèn)我, "你自己為啥不試一下?" 好吧, 我來(lái)試一下但惶。這篇文章就是我嘗試的結(jié)果耳鸯。我要用熟悉的直觀的方法來(lái)解釋Lisp, 我希望有勇氣的人讀
完它, 拿杯飲料, 深呼吸一下, 準(zhǔn)備被搞得暈頭轉(zhuǎn)向。來(lái)吧, 愿你獲得大能膀曾。
重新審視XML
千里之行始于足下县爬。讓我們的第一步從XML開(kāi)始√硪辏可是XML已經(jīng)說(shuō)得更多的了, 還能有什么新意思可說(shuō)呢? 有的财喳。XML自身雖然談?wù)劜簧嫌腥? 但是XML和Lisp的關(guān)系卻相當(dāng)有趣。XML和Lisp的概念有著驚人的相似之處斩狱。XML是我們通向理解Lisp的橋梁纲缓。好吧, 我們且把XML當(dāng)作活馬醫(yī)。讓我們拿好手杖, 對(duì)XML的無(wú)人涉及的荒原地帶作一番探險(xiǎn)喊废。我們要從一個(gè)全新的視角來(lái)考察這個(gè)題目祝高。
表面上看, XML是一種標(biāo)準(zhǔn)化語(yǔ)法, 它以適合人閱讀的格式來(lái)表達(dá)任意的層次化數(shù)據(jù)(hirearchical data)。象任務(wù)表(to-do list), 網(wǎng)頁(yè), 病歷, 汽車保險(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ù)來(lái)表示這種層次化數(shù)據(jù)是很恰當(dāng)?shù)墓す搿Uf(shuō)到底, XML這種比較容易閱讀的數(shù)據(jù)格式, 就是樹(shù)型結(jié)構(gòu)數(shù)據(jù)經(jīng)過(guò)序列化之后的結(jié)果。任何可以用樹(shù)來(lái)表示的數(shù)據(jù), 同樣可以用XML來(lái)表示, 反之亦然瓣蛀。希望你能懂得這一點(diǎn), 這對(duì)下面的內(nèi)容極其重要陆蟆。
再進(jìn)一步。還有什么類型的數(shù)據(jù)也常用樹(shù)來(lái)表示? 無(wú)疑列表(list)也是一種惋增。上過(guò)編譯課吧? 還模模糊糊記得一點(diǎn)吧? 源代碼在解析之后也是用樹(shù)結(jié)構(gòu)來(lái)存放的, 任何編譯程序都會(huì)把源代碼解析成一棵抽象語(yǔ)法樹(shù), 這樣的表示法很恰當(dāng), 因?yàn)樵创a就是層次結(jié)構(gòu)的:函數(shù)包含參數(shù)和代碼塊, 代碼快包含表達(dá)式和語(yǔ)句, 語(yǔ)句包含變量和運(yùn)算符等等叠殷。
我們已經(jīng)知道, 任何樹(shù)結(jié)構(gòu)都可以輕而易舉的寫成XML, 而任何代碼都會(huì)解析成樹(shù), 因此,任何代碼都可以轉(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)單, 用哪種語(yǔ)言來(lái)做都不會(huì)有太大問(wèn)題诈皿。我們可以把任何程序碼轉(zhuǎn)成XML,也可以把XML轉(zhuǎn)回到原來(lái)的程序碼林束。我們可以寫一個(gè)轉(zhuǎn)換器, 把Java代碼轉(zhuǎn)成XML, 另一個(gè)轉(zhuǎn)換器把XML轉(zhuǎn)回到Java。一樣的道理, 這種手段也可以用來(lái)對(duì)付C++(這樣做跟發(fā)瘋差不多么稽亏『埃可是的確有人在做, 看看GCC-XML(http://www.gccxml.org)就知道了)。進(jìn)一步說(shuō),凡是有相同語(yǔ)言特性而語(yǔ)法不同的語(yǔ)言, 都可以把XML當(dāng)作中介來(lái)互相轉(zhuǎn)換代碼截歉。實(shí)際上幾乎所有的主流語(yǔ)言都在一定程度上滿足這個(gè)條件胖腾。我們可以把XML作為一種中間表示法,在兩種語(yǔ)言之間互相譯碼。比方說(shuō), 我們可以用Java2XML把Java代碼轉(zhuǎn)換成XML, 然后用XML2CPP再把XML轉(zhuǎn)換成C++代碼, 運(yùn)氣好的話, 就是說(shuō), 如果我們小心避免使用那些C++不具備的Java特性的話, 我們可以得到完好的C++程序。這辦法怎么樣, 漂亮吧?
這一切充分說(shuō)明, 我們可以把XML作為源代碼的通用存儲(chǔ)方式, 其實(shí)我們能夠產(chǎn)生一整套使用統(tǒng)一語(yǔ)法的程序語(yǔ)言, 也能寫出轉(zhuǎn)換器, 把已有代碼轉(zhuǎn)換成XML格式咸作。如果真的采納這種辦法, 各種語(yǔ)言的編譯器就用不著自己寫語(yǔ)法解析了, 它們可以直接用XML的語(yǔ)法解析來(lái)直接生成抽象語(yǔ)法樹(shù)锨阿。
說(shuō)到這里你該問(wèn)了, 我們研究了這半天XML, 這和Lisp有什么關(guān)系呢? 畢竟XML出來(lái)之時(shí),Lisp早已經(jīng)問(wèn)世三十年了。這里我可以保證, 你馬上就會(huì)明白记罚。不過(guò)在繼續(xù)解釋之前, 我們先做一個(gè)小小的思維練習(xí)群井。看一下上面這個(gè)XML版本的add函數(shù)例子, 你怎樣給它分類,是代碼還是數(shù)據(jù)? 不用太多考慮都能明白, 把它分到哪一類都講得通毫胜。它是XML, 它是標(biāo)準(zhǔn)格式的數(shù)據(jù)。我們也知道, 它可以通過(guò)內(nèi)存中的樹(shù)結(jié)構(gòu)來(lái)生成(GCC-XML做的就是這個(gè)事
情)诬辈。它保存在不可執(zhí)行的文件中酵使。我們可以把它解析成樹(shù)節(jié)點(diǎn), 然后做任意的轉(zhuǎn)換。顯而易見(jiàn), 它是數(shù)據(jù)焙糟。不過(guò)且慢, 雖然它語(yǔ)法有點(diǎn)陌生, 可它又確確實(shí)實(shí)是一個(gè)add函數(shù),對(duì)吧? 一旦經(jīng)過(guò)解析, 它就可以拿給編譯器編譯執(zhí)行口渔。我們可以輕而易舉寫出這個(gè)XML代碼解釋器, 并且直接運(yùn)行它〈┐椋或者我們也可以把它譯成Java或C++代碼, 然后再編譯運(yùn)行缺脉。所以說(shuō), 它也是代碼。
我們說(shuō)到那里了? 不錯(cuò), 我們已經(jīng)發(fā)現(xiàn)了一個(gè)有趣的關(guān)鍵之點(diǎn)悦穿。過(guò)去被認(rèn)為很難解的概念已經(jīng)非常直觀非常簡(jiǎn)單的顯現(xiàn)出來(lái)攻礼。代碼也是數(shù)據(jù), 并且從來(lái)都是如此。這聽(tīng)起來(lái)瘋瘋癲癲的, 實(shí)際上卻是必然之事栗柒。我許諾過(guò)會(huì)以一種全新的方式來(lái)解釋Lisp, 我要重申我的許諾泻轰。但是我們此刻還沒(méi)有到預(yù)定的地方, 所以還是先繼續(xù)上邊的討論盖桥。
剛才我說(shuō)過(guò), 我們可以非常簡(jiǎn)單地實(shí)現(xiàn)XML版的add函數(shù)解釋器, 這聽(tīng)起來(lái)好像不過(guò)是說(shuō)說(shuō)而已。誰(shuí)真的會(huì)動(dòng)手做一下呢? 未必有多少人會(huì)認(rèn)真對(duì)待這件事。隨便說(shuō)說(shuō), 并不打算真的去做, 這樣的事情你在生活中恐怕也遇到吧剧浸。你明白我這樣說(shuō)的意思吧, 我說(shuō)的有沒(méi)有打動(dòng)你? 有哇, 那好, 我們繼續(xù)。
重新審視Ant
我們現(xiàn)在已經(jīng)來(lái)到了月亮背光的那一面, 先別忙著離開(kāi)泞歉。再探索一下, 看看我們還能發(fā)現(xiàn)什么東西雨女。閉上眼睛, 想一想2000年冬天的那個(gè)雨夜, 一個(gè)名叫James Duncan Davidson的杰出的程序員正在研究Tomcat的servlet容器。那時(shí), 他正小心地保存好剛修改過(guò)的文件, 然后執(zhí)行make曙痘。結(jié)果冒出了一大堆錯(cuò)誤, 顯然有什么東西搞錯(cuò)了芳悲。經(jīng)過(guò)仔細(xì)檢查, 他想, 難道是因?yàn)閠ab前面加了個(gè)空格而導(dǎo)致命令不能執(zhí)行嗎? 確實(shí)如此。老是這樣, 他真
的受夠了边坤。烏云背后的月亮給了他啟示, 他創(chuàng)建了一個(gè)新的Java項(xiàng)目, 然后寫了一個(gè)簡(jiǎn)單但是十分有用的工具, 這個(gè)工具巧妙地利用了Java屬性文件中的信息來(lái)構(gòu)造工程, 現(xiàn)在James可以寫makefile的替代品, 它能起到相同的作用, 而形式更加優(yōu)美, 也不用擔(dān)心有makefile那樣可恨的空格問(wèn)題芭概。這個(gè)工具能夠自動(dòng)解釋屬性文件, 然后采取正確的動(dòng)作來(lái)編譯工程。真是簡(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è)月, 他越來(lái)越感到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看起來(lái)是個(gè)不錯(cuò)的選擇桩蓉。他花了幾天工夫把Ant移植到XML淋纲,于是,一件偉大的工具誕生了院究。
Ant是怎樣工作的洽瞬?原理非常簡(jiǎn)單。Ant把包含有構(gòu)造命令的XML文件(算代碼還是算數(shù)據(jù),你自己想吧)业汰,交給一個(gè)Java程序來(lái)解析每一個(gè)元素伙窃,實(shí)際情況比我說(shuō)的還要簡(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類), 通過(guò)調(diào)用Java的方法來(lái)設(shè)置適當(dāng)參數(shù)(todir和fileset)放祟,然后執(zhí)行這個(gè)任務(wù)鳍怨。Ant帶有一組核心類, 可以由用戶任意擴(kuò)展, 只要遵守若干約定就可以。Ant找到這些類,每當(dāng)遇到XML元素有同樣的名字, 就執(zhí)行相應(yīng)的代碼跪妥。過(guò)程非常簡(jiǎn)單鞋喇。Ant做到了我們前面所說(shuō)的東西: 它是一個(gè)語(yǔ)言解釋器, 以XML作為語(yǔ)法, 把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è)工具每天差不多有幾千家公司在使用。
到目前為之, 我還沒(méi)有說(shuō)Ant在解析XML時(shí)所遇到困難执桌。你也不用麻煩去它的網(wǎng)站上去找答案了, 不會(huì)找到有價(jià)值的東西鄙皇。至少對(duì)我們這個(gè)論題來(lái)說(shuō)是如此。我們還是繼續(xù)下一步討論吧仰挣。我們答案就在那里伴逸。
為什么是XML
有時(shí)候正確的決策并非完全出于深思熟慮。我不知道James選擇XML是否出于深思熟慮膘壶。也許僅僅是個(gè)下意識(shí)的決定错蝴。至少?gòu)腏ames在Ant網(wǎng)站上發(fā)表的文章看起來(lái), 他所說(shuō)的理由完全是似是而非。他的主要理由是移植性和擴(kuò)展性, 在Ant案例上, 我看不出這兩條有什么幫助颓芭。使用XML而不是Java代碼, 到底有什么好處? 為什么不寫一組Java類, 提供api來(lái)滿足基本任務(wù)(拷貝目錄, 編譯等等), 然后在Java里直接調(diào)用這些代碼? 這樣做仍然可以保證移植性, 擴(kuò)展性也是毫無(wú)疑問(wèn)的顷锰。而且語(yǔ)法也更為熟悉, 看著順眼。那為什么要用 XML呢? 有什么更好的理由嗎?
有的亡问。雖然我不確定James是否確實(shí)意識(shí)到了官紫。在語(yǔ)義的可構(gòu)造性方面, XML的彈性是Java望塵莫及的肛宋。我不想用高深莫測(cè)的名詞來(lái)嚇唬你, 其中的道理相當(dāng)簡(jiǎn)單, 解釋起來(lái)并不費(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è)代碼看起來(lái)和XML的那個(gè)很相似, 只是稍微長(zhǎng)一點(diǎn)酝陈。差別在那里? 差別在于XML構(gòu)造了一個(gè)特殊的copy動(dòng)詞, 如果我們硬要用Java來(lái)寫的話, 應(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ī)范是定死的, 我們沒(méi)有辦法在程序里改變它沉帮。我們可以增加包, 增加類, 增加方法, 但是我們沒(méi)辦法增加算符,而對(duì)于XML, 我們顯然可以任由自己增加這樣的東西。對(duì)于XML的語(yǔ)法樹(shù)來(lái)說(shuō), 只要原意,我們可以任意增加任何元素, 因此等于我們可以任意增加算符贫堰。如果你還不太明白的話,看下面這個(gè)例子, 加入我們要給Java引入一個(gè)unless算符:
unless(someObject.canFly())
{
someObject.transportByGround():
}
在上面的兩個(gè)例子中, 我們打算給Java語(yǔ)法擴(kuò)展兩個(gè)算符, 成組拷貝文件算符和條件算符unless, 我們要想做到這一點(diǎn), 就必須修改Java編譯器能夠接受的抽象語(yǔ)法樹(shù), 顯然我們無(wú)法用Java標(biāo)準(zhǔn)的功能來(lái)實(shí)現(xiàn)它穆壕。但是在XML中我們可以輕而易舉地做到。我們的解析器根據(jù) XML元素, 生成抽象語(yǔ)法樹(shù), 由此生成算符, 所以, 我們可以任意引入任何算符其屏。
對(duì)于復(fù)雜的算符來(lái)說(shuō), 這樣做的好處顯而易見(jiàn)喇勋。比如, 用特定的算符來(lái)做檢出源碼, 編譯文件, 單元測(cè)試, 發(fā)送郵件等任務(wù), 想想看有多么美妙。對(duì)于特定的題目, 比如說(shuō)構(gòu)造軟件項(xiàng)目, 這些算符的使用可以大幅減低少代碼的數(shù)量漫玄。增加代碼的清晰程度和可重用性。解釋性的XML可以很容易的達(dá)到這個(gè)目標(biāo)压彭。XML是存儲(chǔ)層次化數(shù)據(jù)的簡(jiǎn)單數(shù)據(jù)文件, 而在Java中, 由于層次結(jié)構(gòu)是定死的(你很快就會(huì)看到, Lisp的情況與此截然不同), 我們就沒(méi)法達(dá)到上述目標(biāo)睦优。也許這正是Ant的成功之處呢。
你可以注意一下最近Java和C#的變化(尤其是C#3.0的技術(shù)規(guī)范), C#把常用的功能抽象出來(lái), 作為算符增加到C#中壮不。C#新增加的query算符就是一個(gè)例子汗盘。它用的還是傳統(tǒng)的作法:C#的設(shè)計(jì)者修改抽象語(yǔ)法樹(shù), 然后增加對(duì)應(yīng)的實(shí)現(xiàn)。如果程序員自己也能修改抽象語(yǔ)法樹(shù)該有多好! 那樣我們就可以構(gòu)造用于特定問(wèn)題的子語(yǔ)言(比如說(shuō)就像Ant這種用于構(gòu)造項(xiàng)目的語(yǔ)言), 你能想到別的例子嗎? 再思考一下這個(gè)概念询一。不過(guò)也不必思考太甚, 我們待會(huì)還會(huì)回到這個(gè)題目隐孽。那時(shí)候就會(huì)更加清晰。
離Lisp越來(lái)越近
我們先把算符的事情放一放, 考慮一下Ant設(shè)計(jì)局限之外的東西健蕊。我早先說(shuō)過(guò), Ant可以通過(guò)寫Java類來(lái)擴(kuò)展菱阵。Ant解析器會(huì)根據(jù)名字來(lái)匹配XML元素和Java類, 一旦找到匹配, 就執(zhí)行相應(yīng)任務(wù)。為什么不用Ant自己來(lái)擴(kuò)展Ant呢? 畢竟核心任務(wù)要包含很多傳統(tǒng)語(yǔ)言的結(jié)構(gòu)(例如"if"), 如果Ant自身就能提供構(gòu)造任務(wù)的能力(而不是依賴java類), 我們就可以得到更高的移植性缩功。我們將會(huì)依賴一組核心任務(wù)(如果你原意, 也不妨把它稱作標(biāo)準(zhǔn)庫(kù)), 而
不用管有沒(méi)有Java 環(huán)境了晴及。這組核心任務(wù)可以用任何方式來(lái)實(shí)現(xiàn), 而其他任務(wù)建筑在這組核心任務(wù)之上, 那樣的話, Ant就會(huì)成為通用的, 可擴(kuò)展的, 基于XML的編程語(yǔ)言〉招浚考慮下面這種代碼的可能性:
<task name="Test">
<echo message="Hello World" />
</task>
<Test />
如果XML支持"task"的創(chuàng)建, 上面這段代碼就會(huì)輸出"Hello World!". 實(shí)際上, 我們可以用Java寫個(gè)"task"任務(wù), 然后用Ant-XML來(lái)擴(kuò)展它虑稼。Ant可以在簡(jiǎn)單原語(yǔ)的基礎(chǔ)上寫出更復(fù)雜的原語(yǔ), 就像其他編程語(yǔ)言常用的作法一樣。這也就是我們一開(kāi)始提到的基于XML的編程語(yǔ)言势木。這樣做用處不大(你知道為甚么嗎?), 但是真的很酷蛛倦。
再看一回我們剛才說(shuō)的Task任務(wù)。祝賀你呀, 你在看Lisp代碼!!! 我說(shuō)什么? 一點(diǎn)都不像Lisp嗎? 沒(méi)關(guān)系, 我們?cè)俳o它收拾一下啦桌。
比XML更好
前面一節(jié)說(shuō)過(guò), Ant自我擴(kuò)展沒(méi)什么大用, 原因在于XML很煩瑣溯壶。對(duì)于數(shù)據(jù)來(lái)說(shuō), 這個(gè)問(wèn)題還不太大, 但如果代碼很煩瑣的話, 光是打字上的麻煩就足以抵消它的好處。你寫過(guò)Ant的腳本嗎? 我寫過(guò), 當(dāng)腳本達(dá)到一定復(fù)雜度的時(shí)候, XML非常讓人厭煩。想想看吧, 為了寫結(jié)束標(biāo)簽, 每個(gè)詞都得打兩遍, 不發(fā)瘋算好的!
為了解決這個(gè)問(wèn)題, 我們應(yīng)當(dāng)簡(jiǎn)化寫法茸塞。須知, XML僅僅是一種表達(dá)層次化數(shù)據(jù)的方式躲庄。我們并不是一定要使用尖括號(hào)才能得到樹(shù)的序列化結(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)存里面, 這段代碼的解析樹(shù)在內(nèi)存會(huì)是什么樣子? 會(huì)有一個(gè)"copy"節(jié)點(diǎn), 其下有一個(gè) "fileset"節(jié)點(diǎn), 但是屬性在哪里呢? 它怎樣表達(dá)呢? 如果你以前用過(guò)XML, 并且弄不清楚該用元素還是該用屬性, 你不用感到孤單, 別人一樣糊涂著呢菌仁。沒(méi)人真的搞得清楚浩习。這個(gè)選擇與其說(shuō)是基于技術(shù)的理由, 還不如說(shuō)是閉著眼瞎摸。從概念上來(lái)講, 屬性也是一種元素, 任何屬性能做的, 元素一樣做得到济丘。XML引入屬性的理由, 其實(shí)就是為了讓XML寫法不那么冗長(zhǎng)谱秽。比如我們看個(gè)例子:
<copy>
<toDir>../new/dir</toDir>
<fileset>
<dir>src_dir</dir>
</fileset>
</copy>
兩下比較, 內(nèi)容的信息量完全一樣, 用屬性可以減少打字?jǐn)?shù)量。如果XML沒(méi)有屬性的話,光是打字就夠把人搞瘋掉摹迷。
說(shuō)完了屬性的問(wèn)題, 我們?cè)賮?lái)看一看s表達(dá)式疟赊。之所以繞這么個(gè)彎, 是因?yàn)閟表達(dá)式?jīng)]有屬性的概念。因?yàn)閟表達(dá)式非常簡(jiǎn)練, 根本沒(méi)有必要引入屬性峡碉。我們?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è)元素原來(lái)是有一對(duì)括號(hào)標(biāo)記包圍的, 現(xiàn)在取消了后一個(gè)(就是帶斜杠的那個(gè))括號(hào)標(biāo)記鲫寄。表示元素的結(jié)束只需要一個(gè)")"就可以了吉执。不錯(cuò), 差別就是這些。這兩種表達(dá)方式的轉(zhuǎn)換, 非常自然, 也非常簡(jiǎn)單地来。s表達(dá)式打起字來(lái), 也省事得多戳玫。第一次看s表達(dá)式(Lisp)時(shí), 括號(hào)很煩人是吧? 現(xiàn)在我們明白了背后的道理, 一下子就變得容易多了。至少, 比XML要好的多未斑。用s表達(dá)式寫代碼, 不
單是實(shí)用, 而且也很讓人愉快量九。s表達(dá)式具有XML的一切好處, 這些好處是我們剛剛探討過(guò)的。現(xiàn)在我們看看更加Lisp風(fēng)格的task例子:
(task (name "Test")
(echo (message "Hellow World!")))
(Test)
用Lisp的行話來(lái)講, s表達(dá)式稱為表(list)颂碧。對(duì)于上面的例子, 如果我們寫的時(shí)候不加換行, 用逗號(hào)來(lái)代替空格, 那么這個(gè)表達(dá)式看起來(lái)就非常像一個(gè)元素列表, 其中又嵌套著其他標(biāo)記荠列。
(task, (name, "test"), (echo, (message, "Hello World!")))
XML自然也可以用這樣的風(fēng)格來(lái)寫。當(dāng)然上面這句并不是一般意義上的元素表载城。它實(shí)際上是一個(gè)樹(shù)肌似。這和XML的作用是一樣的。稱它為列表, 希望你不會(huì)感到迷惑, 因?yàn)榍短妆砗蜆?shù)實(shí)際上是一碼事诉瓦。Lisp的字面意思就是表處理(list processing), 其實(shí)也可以稱為樹(shù)處理, 這和處理XML節(jié)點(diǎn)沒(méi)有什么不同川队。
經(jīng)受這一番折磨以后, 現(xiàn)在我們終于相當(dāng)接近Lisp了, Lisp的括號(hào)的神秘本質(zhì)(就像許多Lisp狂熱分子認(rèn)為的)逐漸顯現(xiàn)出來(lái)×ο福現(xiàn)在我們繼續(xù)研究其他內(nèi)容。
重新審視C語(yǔ)言的宏
到了這里, 對(duì)XML的討論你大概都聽(tīng)累了, 我都講累了固额。我們先停一停, 把樹(shù), s表達(dá)式,Ant這些東西先放一放, 我們來(lái)說(shuō)說(shuō)C的預(yù)處理器眠蚂。一定有人問(wèn)了, 我們的話題和C有什么關(guān)系? 我們已經(jīng)知道了很多關(guān)于元編程的事情, 也探討過(guò)專門寫代碼的代碼。理解這問(wèn)題有一定難度, 因?yàn)橄嚓P(guān)討論文章所使用的編程語(yǔ)言, 都是你們不熟悉的斗躏。但是如果只論概念的話, 就相對(duì)要簡(jiǎn)單一些逝慧。我相信, 如果以C語(yǔ)言做例子來(lái)討論元編程, 理解起來(lái)一定會(huì)容易得多。好, 我們接著看啄糙。
一個(gè)問(wèn)題是, 為什么要用代碼來(lái)寫代碼呢? 在實(shí)際的編程中, 怎樣做到這一點(diǎn)呢? 到底元編程是什么意思? 你大概已經(jīng)聽(tīng)說(shuō)過(guò)這些問(wèn)題的答案, 但是并不懂得其中緣由笛臣。為了揭示背后的真理, 我們來(lái)看一下一個(gè)簡(jiǎn)單的數(shù)據(jù)庫(kù)查詢問(wèn)題。這種題目我們都做過(guò)隧饼。比方說(shuō),直接在程序碼里到處寫SQL語(yǔ)句來(lái)修改表(table)里的數(shù)據(jù), 寫多了就非常煩人沈堡。即便用C#3.0的LINQ, 仍然不減其痛苦。寫一個(gè)完整的SQL查詢(盡管語(yǔ)法很優(yōu)美)來(lái)修改某人的地址, 或者查找某人的名字, 絕對(duì)是件令程序員倍感乏味的事情, 那么我們?cè)撛鯓觼?lái)解決這個(gè)問(wèn)題? 答案就是: 使用數(shù)據(jù)訪問(wèn)層燕雁。
概念挺簡(jiǎn)單, 其要點(diǎn)是把數(shù)據(jù)訪問(wèn)的內(nèi)容(至少是那些比較瑣碎的部分)抽象出來(lái), 用類來(lái)映射數(shù)據(jù)庫(kù)的表, 然后用訪問(wèn)對(duì)象屬性訪問(wèn)器(accessor)的辦法來(lái)間接實(shí)現(xiàn)查詢诞丽。這樣就極大地簡(jiǎn)化了開(kāi)發(fā)工作量。我們用訪問(wèn)對(duì)象的方法(或者屬性賦值, 這要視你選用的語(yǔ)言而定)來(lái)代替寫SQL查詢語(yǔ)句拐格。凡是用過(guò)這種方法的人, 都知道這很節(jié)省時(shí)間僧免。當(dāng)然, 如果你要親自寫這樣一個(gè)抽象層, 那可是要花非常多的時(shí)間的--你要寫一組類來(lái)映射表, 把屬性訪問(wèn)轉(zhuǎn)換為SQL查詢, 這個(gè)活相當(dāng)耗費(fèi)精力。用手工來(lái)做顯然是很不明智的禁荒。但是一旦你有了方案和模板, 實(shí)際上就沒(méi)有多少東西需要思考的猬膨。你只需要按照同樣的模板一次又一次重復(fù)編寫相似代碼就可以了角撞。事實(shí)上很多人已經(jīng)發(fā)現(xiàn)了更好的方法, 有一些工具可以幫助你連接數(shù)據(jù)庫(kù), 抓取數(shù)據(jù)庫(kù)結(jié)構(gòu)定義(schema), 按照預(yù)定義的或者用戶定制的模板來(lái)自動(dòng)編寫代碼呛伴。
如果你用過(guò)這種工具, 你肯定會(huì)對(duì)它的神奇效果深為折服。往往只需要鼠標(biāo)點(diǎn)擊數(shù)次, 就可以連接到數(shù)據(jù)庫(kù), 產(chǎn)生數(shù)據(jù)訪問(wèn)源碼, 然后把文件加入到你的工程里面, 十幾分鐘的工作, 按照往常手工方式來(lái)作的話, 也許需要數(shù)百個(gè)小時(shí)人工(man-hours)才能完成谒所∪瓤担可是,如果你的數(shù)據(jù)庫(kù)結(jié)構(gòu)定義后來(lái)改變了怎么辦? 那樣的話, 你只需把這個(gè)過(guò)程重復(fù)一遍就可以了。甚至有一些工具能自動(dòng)完成這項(xiàng)變動(dòng)工作劣领。你只要把它作為工程構(gòu)造的一部分, 每次編譯工程的時(shí)候, 數(shù)據(jù)庫(kù)部分也會(huì)自動(dòng)地重新構(gòu)造姐军。這真的太棒了。你要做的事情基本上減到了0尖淘。如果數(shù)據(jù)庫(kù)結(jié)構(gòu)定義發(fā)生了改變, 并在編譯時(shí)自動(dòng)更新了數(shù)據(jù)訪問(wèn)層的代碼,那么程序中任何使用過(guò)時(shí)的舊代碼的地方, 都會(huì)引發(fā)編譯錯(cuò)誤奕锌。
數(shù)據(jù)訪問(wèn)層是個(gè)很好的例子, 這樣的例子還有好多。從GUI樣板代碼, WEB代碼, COM和CORBA存根, 以及MFC和ATL等等村生。在這些地方, 都是有好多相似代碼多次重復(fù)惊暴。既然這些代碼有可能自動(dòng)編寫, 而程序員時(shí)間又遠(yuǎn)遠(yuǎn)比CPU時(shí)間昂貴, 當(dāng)然就產(chǎn)生了好多工具來(lái)自動(dòng)生成樣板代碼。這些工具的本質(zhì)是什么呢? 它們實(shí)際上就是制造程序的程序趁桃。它們有一個(gè)神秘的名字, 叫做元編程辽话。所謂元編程的本義, 就是如此肄鸽。
元編程本來(lái)可以用到無(wú)數(shù)多的地方, 但實(shí)際上使用的次數(shù)卻沒(méi)有那么多。歸根結(jié)底, 我們心里還是在盤算, 假設(shè)重復(fù)代碼用拷貝粘貼的話, 大概要重復(fù)6,7次, 對(duì)于這樣的工作量,值得專門建立一套生成工具嗎? 當(dāng)然不值得油啤。數(shù)據(jù)訪問(wèn)層和COM存根往往需要重用數(shù)百次,甚至上千次, 所以用工具生成是最好的辦法典徘。而那些僅僅是重復(fù)幾次十幾次的代碼, 是沒(méi)有必要專門做工具的。不必要的時(shí)候也去開(kāi)發(fā)代碼生成工具, 那就顯然過(guò)度估計(jì)了代碼生成的好處益咬。當(dāng)然, 如果創(chuàng)建這類工具足夠簡(jiǎn)單的話, 還是應(yīng)當(dāng)盡量多用, 因?yàn)檫@樣做必然會(huì)節(jié)省時(shí)間〈澹現(xiàn)在來(lái)看一下有沒(méi)有合理的辦法來(lái)達(dá)到這個(gè)目的。
現(xiàn)在, C預(yù)處理器要派上用場(chǎng)了础废。我們都用過(guò)C/C++的預(yù)處理器, 我們用它執(zhí)行簡(jiǎn)單的編譯指令, 來(lái)產(chǎn)生簡(jiǎn)單的代碼變換(比方說(shuō), 設(shè)置調(diào)試代碼開(kāi)關(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ù)處理器再?gòu)?qiáng)大一點(diǎn), 要是能夠允許連接數(shù)據(jù)庫(kù), 要是能多一些其他簡(jiǎn)單的機(jī)制, 我們就可以在我們程序的內(nèi)部開(kāi)發(fā)自己的數(shù)據(jù)訪問(wèn)層帘瞭。下面這個(gè)例子, 是一個(gè)假想的對(duì)C宏的擴(kuò)展:
#get-db-schema("127.0.0.1")V
#iterate-through-tables
#for-each-table
class #table-name
{
};
#end-for-each
我們連接數(shù)據(jù)庫(kù)結(jié)構(gòu)定義, 遍歷數(shù)據(jù)表, 然后對(duì)每個(gè)表創(chuàng)建一個(gè)類, 只消幾行代碼就完成了這個(gè)工作。這樣每次編譯工程的時(shí)候, 這些類都會(huì)根據(jù)數(shù)據(jù)庫(kù)的定義同步更新蒿讥。顯而易見(jiàn), 我們不費(fèi)吹灰之力就在程序內(nèi)部建立了一個(gè)完整的數(shù)據(jù)訪問(wèn)層, 根本用不著任何外部工具蝶念。當(dāng)然這種作法有一個(gè)缺點(diǎn), 那就是我們得學(xué)習(xí)一套新的"編譯時(shí)語(yǔ)言", 另一個(gè)缺點(diǎn)就是根本不存在這么一個(gè)高級(jí)版的C預(yù)處理器。需要做復(fù)雜代碼生成的時(shí)候, 這個(gè)語(yǔ)言(譯者注: 這里指預(yù)處理指令, 即作者所說(shuō)的"編譯時(shí)語(yǔ)言")本身也一定會(huì)變得相當(dāng)復(fù)雜芋绸。它必須支持足夠多的庫(kù)和語(yǔ)言結(jié)構(gòu)媒殉。比如說(shuō)我們想要生成的代碼要依賴某些ftp服務(wù)器上的文件, 預(yù)處理器就得支持ftp訪問(wèn), 僅僅因?yàn)檫@個(gè)任務(wù)而不得不創(chuàng)造和學(xué)習(xí)一門新的語(yǔ)言,真是有點(diǎn)讓人惡心(事實(shí)上已經(jīng)存在著有此能力的語(yǔ)言, 這樣做就更顯荒謬)。我們不妨再靈活一點(diǎn), 為什么不直接用 C/C++自己作為自己的預(yù)處理語(yǔ)言呢? 這樣子的話, 我們可以發(fā)揮語(yǔ)言的強(qiáng)大能力, 要學(xué)的新東西也只不過(guò)是幾個(gè)簡(jiǎn)單的指示字 , 這些指示字用來(lái)區(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ù)處理語(yǔ)言桃犬。我們可以在編譯時(shí)就充分發(fā)揮宿主語(yǔ)言(此處是C/C++)的強(qiáng)大能力, 我們可以很容易地在編譯時(shí)連接數(shù)據(jù)庫(kù), 建立數(shù)據(jù)訪問(wèn)層, 就像JSP或者ASP創(chuàng)建網(wǎng)頁(yè)那樣。我們也用不著專門的窗口工具來(lái)另外建立工程行楞。我們可以在代碼中立即加入必要的工具攒暇。我們也用不著顧慮建立這種工具是不是值得, 因?yàn)檫@太容易了, 太簡(jiǎn)單了。這樣子不知可以節(jié)省多少時(shí)間啊子房。
你好, Lisp
到此刻為止, 我們所知的關(guān)于Lisp的指示可以總結(jié)為一句話: Lisp是一個(gè)可執(zhí)行的語(yǔ)法更優(yōu)美的XML, 但我們還沒(méi)有說(shuō)Lisp是怎樣做到這一點(diǎn)的, 現(xiàn)在開(kāi)始補(bǔ)上這個(gè)話題形用。
Lisp有豐富的內(nèi)置數(shù)據(jù)類型, 其中的整數(shù)和字符串和其他語(yǔ)言沒(méi)什么分別。像71或者"hello"這樣的值, 含義也和C++或者Java這樣的語(yǔ)言大體相同证杭。真正有意思的三種類型是符號(hào)(symbol), 表和函數(shù)田度。這一章的剩余部分, 我都會(huì)用來(lái)介紹這幾種類型, 還要介紹Lisp環(huán)境是怎樣編譯和運(yùn)行源碼的。這個(gè)過(guò)程用Lisp的術(shù)語(yǔ)來(lái)說(shuō)通常叫做求值解愤。通讀這一節(jié)內(nèi)容, 對(duì)于透徹理解元編程的真正潛力, 以及代碼和數(shù)據(jù)的同一性, 和面向領(lǐng)域語(yǔ)言的觀念, 都極其重要镇饺。萬(wàn)勿等閑視之。我會(huì)盡量講得生動(dòng)有趣一些, 也希望你能獲得一些啟發(fā)琢歇。那好, 我們先講符號(hào)兰怠。
大體上, 符號(hào)相當(dāng)于C++或Java語(yǔ)言中的標(biāo)志符, 它的名字可以用來(lái)訪問(wèn)變量值(例如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)任意賦值, 我們這里先用偽碼來(lái)說(shuō)明這一點(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í)怎么辦? 不管怎么說(shuō), 總是乘法呀? 答案簡(jiǎn)單極了。Lisp中函數(shù)的角色十分特殊, 函數(shù)也是一種數(shù)據(jù)類型, 就像整數(shù)和字符串一樣, 因此可以把它賦值給符號(hào)秸侣。乘法函數(shù)Lisp的內(nèi)置函數(shù), 默認(rèn)賦給, 你可以把其他函數(shù)賦值給, 那樣*就不代表乘法了存筏。你也可以把這函數(shù)的值存到另外的變量里。我們?cè)儆脗未a來(lái)說(shuō)明一下:
*(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
這只是舉例子, 我還沒(méi)有詳細(xì)講函數(shù)味榛。Lisp中的函數(shù)是一種數(shù)據(jù)類型, 和整數(shù), 字符串,符號(hào)等等一樣椭坚。一個(gè)函數(shù)并不必然有一個(gè)名字, 這和C++或者Java語(yǔ)言的情形很不相同。在這里函數(shù)自己代表自己搏色。事實(shí)上它是一個(gè)指向代碼塊的指針, 附帶有一些其他信息(例如一組參數(shù)變量)善茎。只有在把函數(shù)賦予其他符號(hào)時(shí), 它才具有了名字, 就像把一個(gè)數(shù)值或字符串賦予變量一樣的道理。你可以用一個(gè)內(nèi)置的專門用于創(chuàng)建函數(shù)的函數(shù)來(lái)創(chuàng)建函數(shù),然后把它賦值給符號(hào)fn, 用偽碼來(lái)表示就是:
fn [a]
{
return *(a, 2);
}
這段代碼返回一個(gè)具有一個(gè)參數(shù)的函數(shù), 函數(shù)的功能是計(jì)算參數(shù)乘2的結(jié)果频轿。這個(gè)函數(shù)還沒(méi)有名字, 你可以把此函數(shù)賦值給別的符號(hào):
set(times-two, fn [a] {return *(a, 2)})
我們現(xiàn)在可以這樣調(diào)用這個(gè)函數(shù):
time-two(5) // 返回10
我們先跳過(guò)符號(hào)和函數(shù), 講一講表垂涯。什么是表? 你也許已經(jīng)聽(tīng)過(guò)好多相關(guān)的說(shuō)法。表, 一言以蔽之, 就是把類似XML那樣的數(shù)據(jù)塊, 用s表達(dá)式來(lái)表示航邢。表用一對(duì)括號(hào)括住, 表中元素以空格分隔, 表可以嵌套耕赘。例如(這回我們用真正的Lisp語(yǔ)法, 注意用分號(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è)元素是字符串
當(dāng)Lisp系統(tǒng)遇到這樣的表時(shí), 它所做的, 和Ant處理XML數(shù)據(jù)所做的, 非常相似, 那就是試圖執(zhí)行它們。其實(shí), Lisp源碼就是特定的一種表, 好比Ant源碼是一種特定的XML一樣膳殷。Lisp執(zhí)行表的順序是這樣的, 表的第一個(gè)元素當(dāng)作函數(shù), 其他元素當(dāng)作函數(shù)的參數(shù)操骡。如果其中某個(gè)參數(shù)也是表, 那就按照同樣的原則對(duì)這個(gè)表求值, 結(jié)果再傳遞給最初的函數(shù)作為參數(shù)。這就是基本原則赚窃。我們看一下真正的代碼:
(* 3 4) ; 相當(dāng)于前面列舉過(guò)的偽碼*(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
上述的例子中, 所有的表都是當(dāng)作代碼來(lái)處理的册招。怎樣把表當(dāng)作數(shù)據(jù)來(lái)處理呢? 同樣的,設(shè)想一下, Ant是把XML數(shù)據(jù)當(dāng)作自己的參數(shù)。在Lisp中, 我們給表加一個(gè)前綴'來(lái)表示數(shù)據(jù)考榨。
(set test '(1 2)) ; test的值為兩元素表
(set test (1 2)) ; 錯(cuò)誤, 1不是函數(shù)
(set test '(* 3 4)) ; test的值是三元素表, 三個(gè)元素分別是*, 3, 4
我們可以用一個(gè)內(nèi)置的函數(shù)head來(lái)返回表的第一個(gè)元素, tail函數(shù)來(lái)返回剩余元素組成的表跨细。
(head '(* 3 4)) ; 返回符號(hào)*
(tail '(* 3 4)) ; 返回表(3 4)
(head (tal '(* 3 4))) ; 返回3
(head test) ; 返回*
你可以把Lisp的內(nèi)置函數(shù)想像成Ant的任務(wù)鹦倚。差別在于, 我們不用在另外的語(yǔ)言中擴(kuò)展Lisp(雖然完全可以做得到), 我們可以用Lisp自己來(lái)擴(kuò)展自己, 就像上面舉的times-two函數(shù)的例子河质。Lisp的內(nèi)置函數(shù)集十分精簡(jiǎn), 只包含了十分必要的部分。剩下的函數(shù)都是作為標(biāo)準(zhǔn)庫(kù)來(lái)實(shí)現(xiàn)的震叙。
Lisp宏
我們已經(jīng)看到, 元編程在一個(gè)類似jsp的模板引擎方面的應(yīng)用掀鹅。我們通過(guò)簡(jiǎn)單的字符串處理來(lái)生成代碼。但是我們可以做的更好媒楼。我們先提一個(gè)問(wèn)題, 怎樣寫一個(gè)工具, 通過(guò)查找目錄結(jié)構(gòu)中的源文件來(lái)自動(dòng)生成Ant腳本乐尊。
用字符串處理的方式生成Ant腳本是一種簡(jiǎn)單的方式。當(dāng)然, 還有一種更加抽象, 表達(dá)能力更強(qiáng), 擴(kuò)展性更好的方式, 就是利用XML庫(kù)在內(nèi)存中直接生成XML節(jié)點(diǎn), 這樣的話內(nèi)存中的節(jié)點(diǎn)就可以自動(dòng)序列化成為字符串划址。不僅如此, 我們的工具還可以分析這些節(jié)點(diǎn), 對(duì)已有的XML文件做變換扔嵌。通過(guò)直接處理XML節(jié)點(diǎn)限府。我們可以超越字符串處理, 使用更高層次的概念, 因此我們的工作就會(huì)做的更快更好。
我們當(dāng)然可以直接用Ant自身來(lái)處理XML變換和制作代碼生成工具痢缎⌒采祝或者我們也可以用Lisp來(lái)做這項(xiàng)工作。正像我們以前所知的, 表是Lisp內(nèi)置的數(shù)據(jù)結(jié)構(gòu), Lisp含有大量的工具來(lái)快速有效的操作表(head和tail是最簡(jiǎn)單的兩個(gè))独旷。而且, Lisp沒(méi)有語(yǔ)義約束, 你可以構(gòu)造任何數(shù)據(jù)結(jié)構(gòu), 只要你原意署穗。
Lisp通過(guò)宏(macro)來(lái)做元編程。我們寫一組宏來(lái)把任務(wù)列表(to-do list)轉(zhuǎn)換為專用領(lǐng)域語(yǔ)言嵌洼。
回想一下上面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ù)并顯示給用戶案疲。在別的語(yǔ)言里(比如說(shuō)Java), 這個(gè)任務(wù)該怎么做? 我們會(huì)解析XML文件, 從中得出任務(wù)表數(shù)據(jù), 然后寫代碼遍歷XML樹(shù), 再轉(zhuǎn)換為Java的數(shù)據(jù)結(jié)構(gòu)(老實(shí)講, 在Java里解析XML真不是件輕松的事情), 最后再把數(shù)據(jù)展示給用戶。現(xiàn)在如果用Lisp, 該怎么做?
假定要用同樣思路的化, 我們大概會(huì)用Lisp庫(kù)來(lái)解析XML麻养。XML對(duì)我們來(lái)說(shuō)就是一個(gè)Lisp的表(s表達(dá)式), 我們可以遍歷這個(gè)表, 然后把相關(guān)數(shù)據(jù)提交給用戶褐啡。可是, 既然我們用Lisp, 就根本沒(méi)有必要再用XML格式保存數(shù)據(jù), 直接用s表達(dá)式就好了, 這樣就沒(méi)有必要做轉(zhuǎn)換了鳖昌。我們也用不著專門的解析庫(kù), Lisp可以直接在內(nèi)存里處理s表達(dá)式春贸。注意, Lisp編譯器和.net編譯器一樣, 對(duì)Lisp程序來(lái)說(shuō), 在運(yùn)行時(shí)總是隨時(shí)可用的。
但是還有更好的辦法遗遵。我們甚至不用寫表達(dá)式來(lái)存儲(chǔ)數(shù)據(jù), 我們可以寫宏, 把數(shù)據(jù)當(dāng)作代碼來(lái)處理萍恕。那該怎么做呢? 真的簡(jiǎn)單〕狄回想一下, Lisp的函數(shù)調(diào)用格式:
(function-name arg1 arg2 arg3)
其中每個(gè)參數(shù)都是s表達(dá)式, 求值以后, 傳遞給函數(shù)允粤。如果我們用(+ 4 5)來(lái)代替arg1,那么, 程序會(huì)先求出結(jié)果, 就是9, 然后把9傳遞給函數(shù)。宏的工作方式和函數(shù)類似翼岁。主要的差別是, 宏的參數(shù)在代入時(shí)不求值类垫。
(macro-name (+ 4 5))
這里, (+ 4 5)作為一個(gè)表傳遞給宏, 然后宏就可以任意處理這個(gè)表, 當(dāng)然也可以對(duì)它求值。宏的返回值是一個(gè)表, 然后有程序作為代碼來(lái)執(zhí)行琅坡。宏所占的位置, 就被替換為這個(gè)結(jié)果代碼悉患。我們可以定義一個(gè)宏把數(shù)據(jù)替換為任意代碼, 比方說(shuō), 替換為顯示數(shù)據(jù)給用戶的代碼。
這和元編程, 以及我們要做的任務(wù)表程序有什么關(guān)系呢? 實(shí)際上, 編譯器會(huì)替我們工作,調(diào)用相應(yīng)的宏榆俺。我們所要做的, 僅僅是創(chuàng)建一個(gè)把數(shù)據(jù)轉(zhuǎn)換為適當(dāng)代碼的宏售躁。
例如, 上面曾經(jīng)將過(guò)的C的求三次方的宏, 用Lisp來(lái)寫是這樣子:
(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ú)有的樂(lè)趣, 很多狂熱分子都熱衷這樣做陪捷。比如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è)非常小的有限的語(yǔ)言來(lái)管理嵌在Lisp中的任務(wù)表市袖。這個(gè)語(yǔ)言只用來(lái)解決特定領(lǐng)域的問(wèn)題, 通常稱之為DSLs(特定領(lǐng)域語(yǔ)言, 或?qū)S妙I(lǐng)域語(yǔ)言)。
特定領(lǐng)域語(yǔ)言
本文談到了兩個(gè)特定領(lǐng)域語(yǔ)言, 一個(gè)是Ant, 處理軟件構(gòu)造烁涌。一個(gè)是沒(méi)起名字的, 用于處理任務(wù)表苍碟。兩者的差別在于, Ant是用XML, XML解析器, 以及Java語(yǔ)言合在一起構(gòu)造出來(lái)的酒觅。而我們的迷你語(yǔ)言則完全內(nèi)嵌在Lisp中, 只消幾分鐘就做出來(lái)了。
我們已經(jīng)說(shuō)過(guò)了DSL的好處, 這也就是Ant用XML而不直接用Java的原因微峰。如果使用Lisp,我們可以任意創(chuàng)建DSL, 只要我們需要阐滩。我們可以創(chuàng)建用于網(wǎng)站程序的DSL, 可以寫多用戶游戲, 做固定收益貿(mào)易(fixed income trade), 解決蛋白質(zhì)折疊問(wèn)題, 處理事務(wù)問(wèn)題, 等等。我們可以把這些疊放在一起, 造出一個(gè)語(yǔ)言, 專門解決基于網(wǎng)絡(luò)的貿(mào)易程序, 既有網(wǎng)絡(luò)語(yǔ)言的優(yōu)勢(shì), 又有貿(mào)易語(yǔ)言的好處县忌。每天我們都會(huì)收獲這種方法帶給我們的益處, 遠(yuǎn)遠(yuǎn)超過(guò)Ant所能給予我們的掂榔。
用DSL解決問(wèn)題, 做出的程序精簡(jiǎn), 易于維護(hù), 富有彈性。在Java里面, 我們可以用類來(lái)處理問(wèn)題症杏。這兩種方法的差別在于, Lisp使我們達(dá)到了一個(gè)更高層次的抽象, 我們不再受語(yǔ)言解析器本身的限制, 比較一下用Java庫(kù)直接寫的構(gòu)造腳本和用Ant寫的構(gòu)造腳本其間的差別装获。同樣的, 比較一下你以前所做的工作, 你就會(huì)明白Lisp帶來(lái)的好處。
接下來(lái)
學(xué)習(xí)Lisp就像戰(zhàn)爭(zhēng)中爭(zhēng)奪山頭厉颤。盡管在電腦科學(xué)領(lǐng)域, Lisp已經(jīng)算是一門古老的語(yǔ)言, 直到現(xiàn)在仍然很少有人真的明白該怎樣給初學(xué)者講授Lisp穴豫。盡管Lisp老手們盡了很大努力,今天新手學(xué)習(xí)Lisp仍然是困難重重。好在現(xiàn)在事情正在發(fā)生變化, Lisp的資源正在迅速增加, 隨著時(shí)間推移, Lisp將會(huì)越來(lái)越受關(guān)注逼友。
Lisp使人超越平庸, 走到前沿精肃。學(xué)會(huì)Lisp意味著你能找到更好的工作, 因?yàn)槁斆鞯墓椭鲿?huì)被你與眾不同的洞察力所打動(dòng)。學(xué)會(huì)Lisp也可能意味著明天你可能會(huì)被解雇, 因?yàn)槟憧偸菑?qiáng)調(diào), 如果公司所有軟件都用Lisp寫, 公司將會(huì)如何卓越, 而這些話你的同事會(huì)聽(tīng)煩的帜乞。Lisp值得努力學(xué)習(xí)嗎? 那些已經(jīng)學(xué)會(huì)Lisp的人都說(shuō)值得, 當(dāng)然, 這取決于你的判斷司抱。
你的看法呢?
這篇文章寫寫停停, 用了幾個(gè)月才最終完成。如果你覺(jué)得有趣, 或者有什么問(wèn)題, 意見(jiàn)或建議, 請(qǐng)給我發(fā)郵件coffeemug@gmail.com, 我會(huì)很高興收到你的反饋黎烈。
1I have never met James, nor does he know about my existence. The story is entirely fictional and is based on a few postings about Ant's history I found on the internet.
2Lisp has many different dialects (the most popular of which are Common Lisp and Scheme). Each dialect deals with intricate details differently yet shares the same set of basic principles. Since the goal of this article is to give you an understanding of Lisp's principles I will use Blaise for examples (which at the time of this writing is vaporware). With some minor modifications these examples can be translated to other Lisp dialects.