說明
函數(shù)式編程
和面向?qū)ο缶幊?/code>可以說是編程的兩大宗教扳躬,猶如編輯器之爭一樣脆诉,之間口角不斷甚亭。我雖然靠著
OOP
的主力語言Java
上班掙錢養(yǎng)活自己,但受到《計算機程序的構(gòu)造和解釋》击胜、《黑客與畫家》的‘蠱惑’以后亏狰,對函數(shù)式編程的所謂完美世界很是心向往之。
最近發(fā)現(xiàn)一篇文章--《Lisp的本質(zhì)》,非常棒偶摔,所以分成幾篇文章暇唾,轉(zhuǎn)過來。
以下為原文:
簡介
最初在web的某些角落偶然看到有人贊美Lisp時, 我那時已經(jīng)是一個頗有經(jīng)驗的程序員辰斋。在我的履歷上, 掌握的語言范圍相當(dāng)廣泛, 象C++, Java, C#主流語言等等都不在話下, 我覺得我差不多知道所有的有關(guān)編程語言的事情策州。對待編程語言的問題上, 我覺得自己不太會遇到什么大問題。其實我大錯特錯了宫仗。
我試著學(xué)了一下Lisp, 結(jié)果馬上就撞了墻够挂。我被那些范例代碼嚇壞了。我想很多初次接觸Lisp語言的人, 一定也有過類似的感受藕夫。Lisp的語法太次了孽糖。一個語言的發(fā)明人, 居然不肯用心弄出一套漂亮的語法, 那誰還會愿意學(xué)它。反正, 我是確確實實被那些難看的無數(shù)
的括號搞蒙了毅贮。
回過神來之后, 我和Lisp社區(qū)的那伙人交談, 訴說我的沮喪心情办悟。結(jié)果, 立馬就有一大套理論砸過來, 這套理論在Lisp社區(qū)處處可見, 幾成慣例。比如說: Lisp的括號只是表面現(xiàn)象; Lisp的代碼和數(shù)據(jù)的表達(dá)方式?jīng)]有差別, 而且比XML語法高明許多, 所以有無窮的好處; Lisp有強大無比的元語言能力, 程序員可以寫出自我維護(hù)的代碼; Lisp可以創(chuàng)造出針對特定應(yīng)用的語言子集; Lisp的運行時和編譯時沒有明確的分界; 等等, 等等, 等等嫩码。這么長的贊美詞雖然看起來相當(dāng)動人, 不過對我毫無意義誉尖。沒人能給我演示這些東西是如何應(yīng)用的, 因為這些東西一般來說只有在大型系統(tǒng)才會用到。我爭辯說, 這些東西傳統(tǒng)語言一樣辦得到铸题。在和別人爭論了數(shù)個小時之后, 我最終還是放棄了學(xué)Lisp的念頭铡恕。為什么要花費幾個月的時間學(xué)習(xí)語法這么難看的語言呢? 這種語言的概念這么晦澀, 又沒什么好懂的例子。也許這語言不是該我這樣的人學(xué)的丢间。
幾個月來, 我承受著這些Lisp辯護(hù)士對我心靈的重壓探熔。我一度陷入了困惑。我認(rèn)識一些絕頂聰明的人, 我對他們相當(dāng)尊敬, 我看到他們對Lisp的贊美達(dá)到了宗教般的高度烘挫。這就是說, Lisp中一定有某種神秘的東西存在, 我不能忍受自己對此的無知, 好奇心和求知欲最
終不可遏制诀艰。我于是咬緊牙關(guān)埋頭學(xué)習(xí)Lisp, 經(jīng)過幾個月的時間費勁心力的練習(xí), 終于,我看到了那無窮無盡的泉水的源頭。在經(jīng)過脫胎換骨的磨練之后, 在經(jīng)過七重地獄的煎熬之后, 終于, 我明白了饮六。
頓悟在突然之間來臨其垄。曾經(jīng)許多次, 我聽到別人引用雷蒙德(譯者注: 譯文<<大教堂和市集>>的作者, 著名的黑客社區(qū)理論家)的話: "Lisp語言值得學(xué)習(xí)。當(dāng)你學(xué)會Lisp之后, 你會擁有深刻的體驗卤橄。就算你平常并不用Lisp編程, 它也會使你成為更加優(yōu)秀的程序員"绿满。過去, 我根本不懂這些話的含義, 我也不相信這是真的】咂耍可是現(xiàn)在我懂得了喇颁。這些話蘊含
的真理遠(yuǎn)遠(yuǎn)超過我過去的想像漏健。我內(nèi)心體會到一種神圣的情感, 一瞬間的頓悟, 幾乎使我對電腦科學(xué)的觀念發(fā)生了根本的改變。
頓悟的那一刻, 我成了Lisp的崇拜者橘霎。我體驗到了宗教大師的感受: 一定要把我的知識傳布開來, 至少要讓10個迷失的靈魂得到拯救蔫浆。按照通常的辦法, 我把這些道理(就是剛開始別人砸過來的那一套, 不過現(xiàn)在我明白了真實的含義)告訴旁人。結(jié)果太令人失望了, 只有少數(shù)幾個人在我堅持之下, 發(fā)生了一點興趣, 但是僅僅看了幾眼Lisp代碼, 他們就退卻了姐叁。照這樣的辦法, 也許費數(shù)年功夫能造就了幾個Lisp迷, 但我覺得這樣的結(jié)果太差強人意了, 我得想一套有更好的辦法瓦盛。
我深入地思考了這個問題。是不是Lisp有什么很艱深的東西, 令得那么多老練的程序員都不能領(lǐng)會? 不是, 沒有任何絕對艱深的東西外潜。因為我能弄懂, 我相信其他人也一定能谭溉。那么問題出在那里? 后來我終于找到了答案。我的結(jié)論就是, 凡是教人學(xué)高級概念, 一定要從他已經(jīng)懂得的東西開始橡卤。如果學(xué)習(xí)過程很有趣, 學(xué)習(xí)的內(nèi)容表達(dá)得很恰當(dāng), 新概念就會變得相當(dāng)直觀。這就是我的答案损搬。所謂元編程, 所謂數(shù)據(jù)和代碼形式合一, 所謂自修改代碼, 所謂特定應(yīng)用的子語言, 所有這些概念根本就是同族概念, 彼此互為解釋, 肯定越講越不明白碧库。還是從實際的例子出發(fā)最有用。
我把我的想法說給Lisp程序員聽, 遭到了他們的反對巧勤。"這些東西本身當(dāng)然不可能用熟悉的知識來解釋, 這些概念完全與眾不同, 你不可能在別人已有的經(jīng)驗里找到類似的東西",可是我認(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ī)布疙。讓我們拿好手杖, 對XML的無人涉及的荒原地帶作一番探險。我們要從一個全新的視角來考察這個題目愿卸。
表面上看, XML是一種標(biāo)準(zhǔn)化語法, 它以適合人閱讀的格式來表達(dá)任意的層次化數(shù)據(jù)(hirearchical data)灵临。象任務(wù)表(to-do list), 網(wǎng)頁, 病歷, 汽車保險單, 配置文件等等, 都是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ù)時會發(fā)生什么情況? 解析之后的數(shù)據(jù)在內(nèi)存中怎樣表示? 顯然, 用樹來表示這種層次化數(shù)據(jù)是很恰當(dāng)?shù)呐枯Uf到底, XML這種比較容易閱讀的數(shù)據(jù)格式, 就是樹型結(jié)構(gòu)數(shù)據(jù)經(jīng)過序列化之后的結(jié)果儒溉。任何可以用樹來表示的數(shù)據(jù), 同樣可以用XML來表示, 反之亦然。希望你能懂得這一點, 這對下面的內(nèi)容極其重要赊舶。
再進(jìn)一步睁搭。還有什么類型的數(shù)據(jù)也常用樹來表示? 無疑列表(list)也是一種赶诊。上過編譯課吧? 還模模糊糊記得一點吧? 源代碼在解析之后也是用樹結(jié)構(gòu)來存放的, 任何編譯程序都會把源代碼解析成一棵抽象語法樹, 這樣的表示法很恰當(dāng), 因為源代碼就是層次結(jié)構(gòu)的:函數(shù)包含參數(shù)和代碼塊, 代碼快包含表達(dá)式和語句, 語句包含變量和運算符等等。
我們已經(jīng)知道, 任何樹結(jié)構(gòu)都可以輕而易舉的寫成XML, 而任何代碼都會解析成樹, 因此,任何代碼都可以轉(zhuǎn)換成XML, 對不對? 我舉個例子, 請看下面的函數(shù):
int add(int arg1, int arg2) {
return arg1+arg2;
}
能把這個函數(shù)變成對等的XML格式嗎? 當(dāng)然可以园骆。我們可以用很多種方式做到, 下面是其中的一種, 十分簡單:
<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>
這個例子非常簡單, 用哪種語言來做都不會有太大問題舔痪。我們可以把任何程序碼轉(zhuǎn)成XML,也可以把XML轉(zhuǎn)回到原來的程序碼。我們可以寫一個轉(zhuǎn)換器, 把Java代碼轉(zhuǎn)成XML, 另一個轉(zhuǎn)換器把XML轉(zhuǎn)回到Java锌唾。一樣的道理, 這種手段也可以用來對付C++(這樣做跟發(fā)瘋差不多么锄码。可是的確有人在做, 看看GCC-XML(http://www.gccxml.org)就知道了)晌涕。進(jìn)一步說,凡是有相同語言特性而語法不同的語言, 都可以把XML當(dāng)作中介來互相轉(zhuǎn)換代碼滋捶。實際上幾乎所有的主流語言都在一定程度上滿足這個條件。我們可以把XML作為一種中間表示法,在兩種語言之間互相譯碼余黎。比方說, 我們可以用Java2XML把Java代碼轉(zhuǎn)換成XML, 然后用XML2CPP再把XML轉(zhuǎn)換成C++代碼, 運氣好的話, 就是說, 如果我們小心避免使用那些C++不具備的Java特性的話, 我們可以得到完好的C++程序重窟。這辦法怎么樣, 漂亮吧?
這一切充分說明, 我們可以把XML作為源代碼的通用存儲方式, 其實我們能夠產(chǎn)生一整套使用統(tǒng)一語法的程序語言, 也能寫出轉(zhuǎn)換器, 把已有代碼轉(zhuǎn)換成XML格式。如果真的采納這種辦法, 各種語言的編譯器就用不著自己寫語法解析了, 它們可以直接用XML的語法解析來直接生成抽象語法樹惧财。
說到這里你該問了, 我們研究了這半天XML, 這和Lisp有什么關(guān)系呢? 畢竟XML出來之時,Lisp早已經(jīng)問世三十年了巡扇。這里我可以保證, 你馬上就會明白。不過在繼續(xù)解釋之前, 我們先做一個小小的思維練習(xí)垮衷√瑁看一下上面這個XML版本的add函數(shù)例子, 你怎樣給它分類,是代碼還是數(shù)據(jù)? 不用太多考慮都能明白, 把它分到哪一類都講得通。它是XML, 它是標(biāo)準(zhǔn)格式的數(shù)據(jù)搀突。我們也知道, 它可以通過內(nèi)存中的樹結(jié)構(gòu)來生成(GCC-XML做的就是這個事
情)刀闷。它保存在不可執(zhí)行的文件中。我們可以把它解析成樹節(jié)點, 然后做任意的轉(zhuǎn)換仰迁。顯而易見, 它是數(shù)據(jù)甸昏。不過且慢, 雖然它語法有點陌生, 可它又確確實實是一個add函數(shù),對吧? 一旦經(jīng)過解析, 它就可以拿給編譯器編譯執(zhí)行。我們可以輕而易舉寫出這個XML代碼解釋器, 并且直接運行它徐许⊥舶牵或者我們也可以把它譯成Java或C++代碼, 然后再編譯運行。所以說, 它也是代碼绊寻。
我們說到那里了? 不錯, 我們已經(jīng)發(fā)現(xiàn)了一個有趣的關(guān)鍵之點花墩。過去被認(rèn)為很難解的概念已經(jīng)非常直觀非常簡單的顯現(xiàn)出來。代碼也是數(shù)據(jù), 并且從來都是如此澄步。這聽起來瘋瘋癲癲的, 實際上卻是必然之事冰蘑。我許諾過會以一種全新的方式來解釋Lisp, 我要重申我的許諾。但是我們此刻還沒有到預(yù)定的地方, 所以還是先繼續(xù)上邊的討論村缸。
剛才我說過, 我們可以非常簡單地實現(xiàn)XML版的add函數(shù)解釋器, 這聽起來好像不過是說說而已祠肥。誰真的會動手做一下呢? 未必有多少人會認(rèn)真對待這件事。隨便說說, 并不打算真的去做, 這樣的事情你在生活中恐怕也遇到吧梯皿。你明白我這樣說的意思吧, 我說的有沒有打動你? 有哇, 那好, 我們繼續(xù)仇箱。
重新審視Ant
我們現(xiàn)在已經(jīng)來到了月亮背光的那一面, 先別忙著離開县恕。再探索一下, 看看我們還能發(fā)現(xiàn)什么東西。閉上眼睛, 想一想2000年冬天的那個雨夜, 一個名叫James Duncan Davidson的杰出的程序員正在研究Tomcat的servlet容器剂桥。那時, 他正小心地保存好剛修改過的文件, 然后執(zhí)行make忠烛。結(jié)果冒出了一大堆錯誤, 顯然有什么東西搞錯了。經(jīng)過仔細(xì)檢查, 他想, 難道是因為tab前面加了個空格而導(dǎo)致命令不能執(zhí)行嗎? 確實如此权逗。老是這樣, 他真
的受夠了美尸。烏云背后的月亮給了他啟示, 他創(chuàng)建了一個新的Java項目, 然后寫了一個簡單但是十分有用的工具, 這個工具巧妙地利用了Java屬性文件中的信息來構(gòu)造工程, 現(xiàn)在James可以寫makefile的替代品, 它能起到相同的作用, 而形式更加優(yōu)美, 也不用擔(dān)心有makefile那樣可恨的空格問題。這個工具能夠自動解釋屬性文件, 然后采取正確的動作來編譯工程斟薇。真是簡單而優(yōu)美师坎。
(作者注: 我不認(rèn)識James, James也不認(rèn)識我, 這個故事是根據(jù)網(wǎng)上關(guān)于Ant歷史的帖子虛構(gòu)的)
使用Ant構(gòu)造Tomcat之后幾個月, 他越來越感到Java的屬性文件不足以表達(dá)復(fù)雜的構(gòu)造指令。文件需要檢出, 拷貝, 編譯, 發(fā)到另外一臺機器, 進(jìn)行單元測試堪滨。要是出錯, 就發(fā)郵件給相關(guān)人員, 要是成功, 就繼續(xù)在盡可能高層的卷(volumn)上執(zhí)行構(gòu)造胯陋。追蹤到最后,卷要回復(fù)到最初的水平上。確實, Java的屬性文件不夠用了, James需要更有彈性的解決方案袱箱。他不想自己寫解析器(因為他更希望有一個具有工業(yè)標(biāo)準(zhǔn)的方案)惶岭。XML看起來是個不錯的選擇。他花了幾天工夫把Ant移植到XML犯眠,于是,一件偉大的工具誕生了症革。
Ant是怎樣工作的筐咧?原理非常簡單。Ant把包含有構(gòu)造命令的XML文件(算代碼還是算數(shù)據(jù),你自己想吧)噪矛,交給一個Java程序來解析每一個元素,實際情況比我說的還要簡單得多艇挨。一個簡單的XML指令會導(dǎo)致具有相同名字的Java類裝入残炮,并執(zhí)行其代碼。
<copy todir="../new/dir">
<fileset dir="src_dir" />
</copy>
這段文字的含義是把源目錄復(fù)制到目標(biāo)目錄缩滨,Ant會找到一個"copy"任務(wù)(實際上就是一個Java類), 通過調(diào)用Java的方法來設(shè)置適當(dāng)參數(shù)(todir和fileset)势就,然后執(zhí)行這個任務(wù)。Ant帶有一組核心類, 可以由用戶任意擴展, 只要遵守若干約定就可以脉漏。Ant找到這些類,每當(dāng)遇到XML元素有同樣的名字, 就執(zhí)行相應(yīng)的代碼苞冯。過程非常簡單。Ant做到了我們前面所說的東西: 它是一個語言解釋器, 以XML作為語法, 把XML元素轉(zhuǎn)譯為適當(dāng)?shù)腏ava指令侧巨。我們可以寫一個"add"任務(wù), 然后, 當(dāng)發(fā)現(xiàn)XML中有add描述的時候, 就執(zhí)行這個add任務(wù)舅锄。由于Ant是非常流行的項目, 前面展示的策略就顯得更為明智。畢竟, 這個工具每天差不多有幾千家公司在使用司忱。
到目前為之, 我還沒有說Ant在解析XML時所遇到困難皇忿。你也不用麻煩去它的網(wǎng)站上去找答案了, 不會找到有價值的東西畴蹭。至少對我們這個論題來說是如此。我們還是繼續(xù)下一步討論吧鳍烁。我們答案就在那里叨襟。
為什么是XML
有時候正確的決策并非完全出于深思熟慮。我不知道James選擇XML是否出于深思熟慮老翘。也許僅僅是個下意識的決定芹啥。至少從James在Ant網(wǎng)站上發(fā)表的文章看起來, 他所說的理由完全是似是而非。他的主要理由是移植性和擴展性, 在Ant案例上, 我看不出這兩條有什么幫助铺峭。使用XML而不是Java代碼, 到底有什么好處? 為什么不寫一組Java類, 提供api來滿足基本任務(wù)(拷貝目錄, 編譯等等), 然后在Java里直接調(diào)用這些代碼? 這樣做仍然可以保證移植性, 擴展性也是毫無疑問的墓怀。而且語法也更為熟悉, 看著順眼。那為什么要用 XML呢? 有什么更好的理由嗎?
有的。雖然我不確定James是否確實意識到了。在語義的可構(gòu)造性方面, XML的彈性是Java望塵莫及的摘符。我不想用高深莫測的名詞來嚇唬你, 其中的道理相當(dāng)簡單, 解釋起來并不費很多功夫鸣皂。好, 做好預(yù)備動作, 我們馬上就要朝向頓悟的時刻做奮力一躍。
上面的那個copy的例子, 用Java代碼怎樣實現(xiàn)呢? 我們可以這樣做:
CopyTask copy = new CopyTask();
Fileset fileset = new Fileset();
fileset.setDir("src_dir");
copy.setToDir("../new/dir");
copy.setFileset(fileset);
copy.excute();
這個代碼看起來和XML的那個很相似, 只是稍微長一點垒手。差別在那里? 差別在于XML構(gòu)造了一個特殊的copy動詞, 如果我們硬要用Java來寫的話, 應(yīng)該是這個樣子:
copy("../new/dir");
{
fileset("src_dir");
}
看到差別了嗎? 以上代碼(如果可以在Java中用的化), 是一個特殊的copy算符, 有點像for循環(huán)或者Java5中的foreach循環(huán)。如果我們有一個轉(zhuǎn)換器, 可以把XML轉(zhuǎn)換到Java, 大概就會得到上面這段事實上不可以執(zhí)行的代碼。因為Java的技術(shù)規(guī)范是定死的, 我們沒有辦法在程序里改變它梆暮。我們可以增加包, 增加類, 增加方法, 但是我們沒辦法增加算符,而對于XML, 我們顯然可以任由自己增加這樣的東西。對于XML的語法樹來說, 只要原意,我們可以任意增加任何元素, 因此等于我們可以任意增加算符绍昂。如果你還不太明白的話,看下面這個例子, 加入我們要給Java引入一個unless算符:
unless(someObject.canFly())
{
someObject.transportByGround():
}
在上面的兩個例子中, 我們打算給Java語法擴展兩個算符, 成組拷貝文件算符和條件算符unless, 我們要想做到這一點, 就必須修改Java編譯器能夠接受的抽象語法樹, 顯然我們無法用Java標(biāo)準(zhǔn)的功能來實現(xiàn)它啦粹。但是在XML中我們可以輕而易舉地做到。我們的解析器根據(jù) XML元素, 生成抽象語法樹, 由此生成算符, 所以, 我們可以任意引入任何算符窘游。
對于復(fù)雜的算符來說, 這樣做的好處顯而易見唠椭。比如, 用特定的算符來做檢出源碼, 編譯文件, 單元測試, 發(fā)送郵件等任務(wù), 想想看有多么美妙。對于特定的題目, 比如說構(gòu)造軟件項目, 這些算符的使用可以大幅減低少代碼的數(shù)量忍饰。增加代碼的清晰程度和可重用性贪嫂。解釋性的XML可以很容易的達(dá)到這個目標(biāo)。XML是存儲層次化數(shù)據(jù)的簡單數(shù)據(jù)文件, 而在Java中, 由于層次結(jié)構(gòu)是定死的(你很快就會看到, Lisp的情況與此截然不同), 我們就沒法達(dá)到上述目標(biāo)艾蓝。也許這正是Ant的成功之處呢力崇。
你可以注意一下最近Java和C#的變化(尤其是C#3.0的技術(shù)規(guī)范), C#把常用的功能抽象出來, 作為算符增加到C#中。C#新增加的query算符就是一個例子赢织。它用的還是傳統(tǒng)的作法:C#的設(shè)計者修改抽象語法樹, 然后增加對應(yīng)的實現(xiàn)餐曹。如果程序員自己也能修改抽象語法樹該有多好! 那樣我們就可以構(gòu)造用于特定問題的子語言(比如說就像Ant這種用于構(gòu)造項目的語言), 你能想到別的例子嗎? 再思考一下這個概念。不過也不必思考太甚, 我們待會還會回到這個題目敌厘。那時候就會更加清晰。
離Lisp越來越近
我們先把算符的事情放一放, 考慮一下Ant設(shè)計局限之外的東西曹步。我早先說過, Ant可以通過寫Java類來擴展。Ant解析器會根據(jù)名字來匹配XML元素和Java類, 一旦找到匹配, 就執(zhí)行相應(yīng)任務(wù)筹麸。為什么不用Ant自己來擴展Ant呢? 畢竟核心任務(wù)要包含很多傳統(tǒng)語言的結(jié)構(gòu)(例如"if"), 如果Ant自身就能提供構(gòu)造任務(wù)的能力(而不是依賴java類), 我們就可以得到更高的移植性留晚。我們將會依賴一組核心任務(wù)(如果你原意, 也不妨把它稱作標(biāo)準(zhǔn)庫), 而
不用管有沒有Java 環(huán)境了奖地。這組核心任務(wù)可以用任何方式來實現(xiàn), 而其他任務(wù)建筑在這組核心任務(wù)之上, 那樣的話, Ant就會成為通用的, 可擴展的, 基于XML的編程語言参歹∪樱考慮下面這種代碼的可能性:
<task name="Test">
<echo message="Hello World" />
</task>
<Test />
如果XML支持"task"的創(chuàng)建, 上面這段代碼就會輸出"Hello World!". 實際上, 我們可以用Java寫個"task"任務(wù), 然后用Ant-XML來擴展它捎泻。Ant可以在簡單原語的基礎(chǔ)上寫出更復(fù)雜的原語, 就像其他編程語言常用的作法一樣笆豁。這也就是我們一開始提到的基于XML的編程語言煞赢。這樣做用處不大(你知道為甚么嗎?), 但是真的很酷。
再看一回我們剛才說的Task任務(wù)波俄。祝賀你呀, 你在看Lisp代碼!!! 我說什么? 一點都不像Lisp嗎? 沒關(guān)系, 我們再給它收拾一下支鸡。
比XML更好
前面一節(jié)說過, Ant自我擴展沒什么大用, 原因在于XML很煩瑣牧挣。對于數(shù)據(jù)來說, 這個問題還不太大, 但如果代碼很煩瑣的話, 光是打字上的麻煩就足以抵消它的好處。你寫過Ant的腳本嗎? 我寫過, 當(dāng)腳本達(dá)到一定復(fù)雜度的時候, XML非常讓人厭煩叔汁。想想看吧, 為了寫結(jié)束標(biāo)簽, 每個詞都得打兩遍, 不發(fā)瘋算好的!
為了解決這個問題, 我們應(yīng)當(dāng)簡化寫法检碗。須知, XML僅僅是一種表達(dá)層次化數(shù)據(jù)的方式。我們并不是一定要使用尖括號才能得到樹的序列化結(jié)果折剃。我們完全可以采用其他的格式另假。其中的一種(剛好就是Lisp所采用的)格式, 叫做s表達(dá)式怕犁。s表達(dá)式要做的和XML一樣, 但它的好處是寫法更簡單, 簡單的寫法更適合代碼輸入奏甫。后面我會詳細(xì)講s表達(dá)式。這之前我要清理一下XML的東西挠进∨担考慮一下關(guān)于拷貝文件的例子:
<copy toDir="../new/dir">
<fileset dir="src_dir">
</copy>
想想看在內(nèi)存里面, 這段代碼的解析樹在內(nèi)存會是什么樣子? 會有一個"copy"節(jié)點, 其下有一個 "fileset"節(jié)點, 但是屬性在哪里呢? 它怎樣表達(dá)呢? 如果你以前用過XML, 并且弄不清楚該用元素還是該用屬性, 你不用感到孤單, 別人一樣糊涂著呢。沒人真的搞得清楚君旦。這個選擇與其說是基于技術(shù)的理由, 還不如說是閉著眼瞎摸澎办。從概念上來講, 屬性也是一種元素, 任何屬性能做的, 元素一樣做得到嘲碱。XML引入屬性的理由, 其實就是為了讓XML寫法不那么冗長。比如我們看個例子:
<copy>
<toDir>../new/dir</toDir>
<fileset>
<dir>src_dir</dir>
</fileset>
</copy>
兩下比較, 內(nèi)容的信息量完全一樣, 用屬性可以減少打字?jǐn)?shù)量浮驳。如果XML沒有屬性的話,光是打字就夠把人搞瘋掉悍汛。
說完了屬性的問題, 我們再來看一看s表達(dá)式。之所以繞這么個彎, 是因為s表達(dá)式?jīng)]有屬性的概念至会。因為s表達(dá)式非常簡練, 根本沒有必要引入屬性离咐。我們在把XML轉(zhuǎn)換成s表達(dá)式的時候, 心里應(yīng)該記住這一點》罴看個例子, 上面的代碼譯成s表達(dá)式是這樣的:
(copy
(todir "../new/dir")
(fileset (dir "src_dir")))
仔細(xì)看看這個例子, 差別在哪里? 尖括號改成了圓括號, 每個元素原來是有一對括號標(biāo)記包圍的, 現(xiàn)在取消了后一個(就是帶斜杠的那個)括號標(biāo)記宵蛀。表示元素的結(jié)束只需要一個")"就可以了。不錯, 差別就是這些县貌。這兩種表達(dá)方式的轉(zhuǎn)換, 非常自然, 也非常簡單术陶。s表達(dá)式打起字來, 也省事得多。第一次看s表達(dá)式(Lisp)時, 括號很煩人是吧? 現(xiàn)在我們明白了背后的道理, 一下子就變得容易多了煤痕。至少, 比XML要好的多梧宫。用s表達(dá)式寫代碼, 不
單是實用, 而且也很讓人愉快。s表達(dá)式具有XML的一切好處, 這些好處是我們剛剛探討過的“诘铮現(xiàn)在我們看看更加Lisp風(fēng)格的task例子:
(task (name "Test")
(echo (message "Hellow World!")))
(Test)
用Lisp的行話來講, s表達(dá)式稱為表(list)塘匣。對于上面的例子, 如果我們寫的時候不加換行, 用逗號來代替空格, 那么這個表達(dá)式看起來就非常像一個元素列表, 其中又嵌套著其他標(biāo)記。
(task, (name, "test"), (echo, (message, "Hello World!")))
XML自然也可以用這樣的風(fēng)格來寫巷帝。當(dāng)然上面這句并不是一般意義上的元素表忌卤。它實際上是一個樹。這和XML的作用是一樣的楞泼。稱它為列表, 希望你不會感到迷惑, 因為嵌套表和樹實際上是一碼事驰徊。Lisp的字面意思就是表處理(list processing), 其實也可以稱為樹處理, 這和處理XML節(jié)點沒有什么不同。
經(jīng)受這一番折磨以后, 現(xiàn)在我們終于相當(dāng)接近Lisp了, Lisp的括號的神秘本質(zhì)(就像許多Lisp狂熱分子認(rèn)為的)逐漸顯現(xiàn)出來《槔現(xiàn)在我們繼續(xù)研究其他內(nèi)容棍厂。
重新審視C語言的宏
到了這里, 對XML的討論你大概都聽累了, 我都講累了。我們先停一停, 把樹, s表達(dá)式,Ant這些東西先放一放, 我們來說說C的預(yù)處理器超陆。一定有人問了, 我們的話題和C有什么關(guān)系? 我們已經(jīng)知道了很多關(guān)于元編程的事情, 也探討過專門寫代碼的代碼牺弹。理解這問題有一定難度, 因為相關(guān)討論文章所使用的編程語言, 都是你們不熟悉的。但是如果只論概念的話, 就相對要簡單一些侥猬。我相信, 如果以C語言做例子來討論元編程, 理解起來一定會容易得多例驹。好, 我們接著看捐韩。
一個問題是, 為什么要用代碼來寫代碼呢? 在實際的編程中, 怎樣做到這一點呢? 到底元編程是什么意思? 你大概已經(jīng)聽說過這些問題的答案, 但是并不懂得其中緣由退唠。為了揭示背后的真理, 我們來看一下一個簡單的數(shù)據(jù)庫查詢問題。這種題目我們都做過荤胁。比方說,直接在程序碼里到處寫SQL語句來修改表(table)里的數(shù)據(jù), 寫多了就非常煩人瞧预。即便用C#3.0的LINQ, 仍然不減其痛苦。寫一個完整的SQL查詢(盡管語法很優(yōu)美)來修改某人的地址, 或者查找某人的名字, 絕對是件令程序員倍感乏味的事情, 那么我們該怎樣來解決這個問題? 答案就是: 使用數(shù)據(jù)訪問層。
概念挺簡單, 其要點是把數(shù)據(jù)訪問的內(nèi)容(至少是那些比較瑣碎的部分)抽象出來, 用類來映射數(shù)據(jù)庫的表, 然后用訪問對象屬性訪問器(accessor)的辦法來間接實現(xiàn)查詢垢油。這樣就極大地簡化了開發(fā)工作量盆驹。我們用訪問對象的方法(或者屬性賦值, 這要視你選用的語言而定)來代替寫SQL查詢語句。凡是用過這種方法的人, 都知道這很節(jié)省時間滩愁。當(dāng)然, 如果你要親自寫這樣一個抽象層, 那可是要花非常多的時間的--你要寫一組類來映射表, 把屬性訪問轉(zhuǎn)換為SQL查詢, 這個活相當(dāng)耗費精力躯喇。用手工來做顯然是很不明智的。但是一旦你有了方案和模板, 實際上就沒有多少東西需要思考的硝枉。你只需要按照同樣的模板一次又一次重復(fù)編寫相似代碼就可以了廉丽。事實上很多人已經(jīng)發(fā)現(xiàn)了更好的方法, 有一些工具可以幫助你連接數(shù)據(jù)庫, 抓取數(shù)據(jù)庫結(jié)構(gòu)定義(schema), 按照預(yù)定義的或者用戶定制的模板來自動編寫代碼。
如果你用過這種工具, 你肯定會對它的神奇效果深為折服妻味。往往只需要鼠標(biāo)點擊數(shù)次, 就可以連接到數(shù)據(jù)庫, 產(chǎn)生數(shù)據(jù)訪問源碼, 然后把文件加入到你的工程里面, 十幾分鐘的工作, 按照往常手工方式來作的話, 也許需要數(shù)百個小時人工(man-hours)才能完成正压。可是,如果你的數(shù)據(jù)庫結(jié)構(gòu)定義后來改變了怎么辦? 那樣的話, 你只需把這個過程重復(fù)一遍就可以了责球。甚至有一些工具能自動完成這項變動工作焦履。你只要把它作為工程構(gòu)造的一部分, 每次編譯工程的時候, 數(shù)據(jù)庫部分也會自動地重新構(gòu)造。這真的太棒了雏逾。你要做的事情基本上減到了0嘉裤。如果數(shù)據(jù)庫結(jié)構(gòu)定義發(fā)生了改變, 并在編譯時自動更新了數(shù)據(jù)訪問層的代碼,那么程序中任何使用過時的舊代碼的地方, 都會引發(fā)編譯錯誤。
數(shù)據(jù)訪問層是個很好的例子, 這樣的例子還有好多校套。從GUI樣板代碼, WEB代碼, COM和CORBA存根, 以及MFC和ATL等等价脾。在這些地方, 都是有好多相似代碼多次重復(fù)。既然這些代碼有可能自動編寫, 而程序員時間又遠(yuǎn)遠(yuǎn)比CPU時間昂貴, 當(dāng)然就產(chǎn)生了好多工具來自動生成樣板代碼笛匙。這些工具的本質(zhì)是什么呢? 它們實際上就是制造程序的程序侨把。它們有一個神秘的名字, 叫做元編程。所謂元編程的本義, 就是如此妹孙。
元編程本來可以用到無數(shù)多的地方, 但實際上使用的次數(shù)卻沒有那么多秋柄。歸根結(jié)底, 我們心里還是在盤算, 假設(shè)重復(fù)代碼用拷貝粘貼的話, 大概要重復(fù)6,7次, 對于這樣的工作量,值得專門建立一套生成工具嗎? 當(dāng)然不值得。數(shù)據(jù)訪問層和COM存根往往需要重用數(shù)百次,甚至上千次, 所以用工具生成是最好的辦法蠢正。而那些僅僅是重復(fù)幾次十幾次的代碼, 是沒有必要專門做工具的骇笔。不必要的時候也去開發(fā)代碼生成工具, 那就顯然過度估計了代碼生成的好處。當(dāng)然, 如果創(chuàng)建這類工具足夠簡單的話, 還是應(yīng)當(dāng)盡量多用, 因為這樣做必然會節(jié)省時間∠福現(xiàn)在來看一下有沒有合理的辦法來達(dá)到這個目的笨触。
現(xiàn)在, C預(yù)處理器要派上用場了。我們都用過C/C++的預(yù)處理器, 我們用它執(zhí)行簡單的編譯指令, 來產(chǎn)生簡單的代碼變換(比方說, 設(shè)置調(diào)試代碼開關(guān)), 看一個例子:
#define triple(X) X+X+X
這一行的作用是什么? 這是一個簡單的預(yù)編譯指令, 它把程序中的triple(X)替換稱為X+X+X雹舀。例如, 把所有triple(5)都換成5+5+5, 然后再交給編譯器編譯芦劣。這就是一個簡單的代碼生成的例子。要是C的預(yù)處理器再強大一點, 要是能夠允許連接數(shù)據(jù)庫, 要是能多一些其他簡單的機制, 我們就可以在我們程序的內(nèi)部開發(fā)自己的數(shù)據(jù)訪問層说榆。下面這個例子, 是一個假想的對C宏的擴展:
#get-db-schema("127.0.0.1")V
#iterate-through-tables
#for-each-table
class #table-name
{
};
#end-for-each
我們連接數(shù)據(jù)庫結(jié)構(gòu)定義, 遍歷數(shù)據(jù)表, 然后對每個表創(chuàng)建一個類, 只消幾行代碼就完成了這個工作虚吟。這樣每次編譯工程的時候, 這些類都會根據(jù)數(shù)據(jù)庫的定義同步更新寸认。顯而易見, 我們不費吹灰之力就在程序內(nèi)部建立了一個完整的數(shù)據(jù)訪問層, 根本用不著任何外部工具。當(dāng)然這種作法有一個缺點, 那就是我們得學(xué)習(xí)一套新的"編譯時語言", 另一個缺點就是根本不存在這么一個高級版的C預(yù)處理器串慰。需要做復(fù)雜代碼生成的時候, 這個語言(譯者注: 這里指預(yù)處理指令, 即作者所說的"編譯時語言")本身也一定會變得相當(dāng)復(fù)雜偏塞。它必須支持足夠多的庫和語言結(jié)構(gòu)。比如說我們想要生成的代碼要依賴某些ftp服務(wù)器上的文件, 預(yù)處理器就得支持ftp訪問, 僅僅因為這個任務(wù)而不得不創(chuàng)造和學(xué)習(xí)一門新的語言,真是有點讓人惡心(事實上已經(jīng)存在著有此能力的語言, 這樣做就更顯荒謬)邦鲫。我們不妨再靈活一點, 為什么不直接用 C/C++自己作為自己的預(yù)處理語言呢? 這樣子的話, 我們可以發(fā)揮語言的強大能力, 要學(xué)的新東西也只不過是幾個簡單的指示字 , 這些指示字用來區(qū)別編譯時代碼和運行時代碼灸叼。
<%
cout<<"Enter a number: ";
cin>>n;
%>
for(int i=0;i< <% n %>;i++)
{
cout<<"hello"<<endl;
}
你明白了嗎? 在<%和%>標(biāo)記之間的代碼是在編譯時運行的, 標(biāo)記之外的其他代碼都是普通代碼。編譯程序時, 系統(tǒng)會提示你輸入一個數(shù), 這個數(shù)在后面的循環(huán)中會用到庆捺。而for循環(huán)的代碼會被編譯怜姿。假定你在編譯時輸入5, for循環(huán)的代碼將會是:
for(int i=0;i<5; i++)
{
cout<<"hello"<<endl;
}
又簡單又有效率, 也不需要另外的預(yù)處理語言。我們可以在編譯時就充分發(fā)揮宿主語言(此處是C/C++)的強大能力, 我們可以很容易地在編譯時連接數(shù)據(jù)庫, 建立數(shù)據(jù)訪問層, 就像JSP或者ASP創(chuàng)建網(wǎng)頁那樣疼燥。我們也用不著專門的窗口工具來另外建立工程沧卢。我們可以在代碼中立即加入必要的工具。我們也用不著顧慮建立這種工具是不是值得, 因為這太容易了, 太簡單了醉者。這樣子不知可以節(jié)省多少時間啊但狭。
你好, Lisp
到此刻為止, 我們所知的關(guān)于Lisp的指示可以總結(jié)為一句話: Lisp是一個可執(zhí)行的語法更優(yōu)美的XML, 但我們還沒有說Lisp是怎樣做到這一點的, 現(xiàn)在開始補上這個話題。
Lisp有豐富的內(nèi)置數(shù)據(jù)類型, 其中的整數(shù)和字符串和其他語言沒什么分別撬即。像71或者"hello"這樣的值, 含義也和C++或者Java這樣的語言大體相同立磁。真正有意思的三種類型是符號(symbol), 表和函數(shù)。這一章的剩余部分, 我都會用來介紹這幾種類型, 還要介紹Lisp環(huán)境是怎樣編譯和運行源碼的剥槐。這個過程用Lisp的術(shù)語來說通常叫做求值唱歧。通讀這一節(jié)內(nèi)容, 對于透徹理解元編程的真正潛力, 以及代碼和數(shù)據(jù)的同一性, 和面向領(lǐng)域語言的觀念, 都極其重要。萬勿等閑視之粒竖。我會盡量講得生動有趣一些, 也希望你能獲得一些啟發(fā)颅崩。那好, 我們先講符號。
大體上, 符號相當(dāng)于C++或Java語言中的標(biāo)志符, 它的名字可以用來訪問變量值(例如currentTime, arrayCount, n, 等等), 差別在于, Lisp中的符號更加基本蕊苗。在C++或Java里面, 變量名只能用字母和下劃線的組合, 而Lisp的符號則非常有包容性, 比如, 加號(+)就是一個合法的符號, 其他的像-, =, hello-world, *等等都可以是符號名沿后。符號名的命名規(guī)則可以在網(wǎng)上查到。你可以給這些符號任意賦值, 我們這里先用偽碼來說明這一點朽砰。假定函數(shù)set是給變量賦值(就像等號=在C++和Java里的作用), 下面是我們的例子:
set(test, 5) // 符號test的值為5
set(=, 5) // 符號=的值為5
set(test, "hello") // 符號test的值為字符串"hello"
set(test, =) // 此時符號=的值為5, 所以test的也為5
set(*, "hello") // 符號*的值為"hello"
好像有什么不對的地方? 假定我們對賦給整數(shù)或者字符串值, 那做乘法時怎么辦? 不管怎么說, 總是乘法呀? 答案簡單極了尖滚。Lisp中函數(shù)的角色十分特殊, 函數(shù)也是一種數(shù)據(jù)類型, 就像整數(shù)和字符串一樣, 因此可以把它賦值給符號。乘法函數(shù)Lisp的內(nèi)置函數(shù), 默認(rèn)賦給, 你可以把其他函數(shù)賦值給, 那樣*就不代表乘法了瞧柔。你也可以把這函數(shù)的值存到另外的變量里漆弄。我們再用偽碼來說明一下:
*(3,4) // 3乘4, 結(jié)果是12
set(temp, *) // 把*的值, 也就是乘法函數(shù), 賦值給temp
set(*, 3) // 把3賦予*
*(3,4) // 錯誤的表達(dá)式, *不再是乘法, 而是數(shù)值3
temp(3,4) // temp是乘法函數(shù), 所以此表達(dá)式的值為3乘4等于12
set(*, temp) // 再次把乘法函數(shù)賦予*
*(3,4) // 3乘4等于12
再古怪一點, 把減號的值賦給加號:
set(+, -) // 減號(-)是內(nèi)置的減法函數(shù)
+(5, 4) // 加號(+)現(xiàn)在是代表減法函數(shù), 結(jié)果是5減4等于1
這只是舉例子, 我還沒有詳細(xì)講函數(shù)。Lisp中的函數(shù)是一種數(shù)據(jù)類型, 和整數(shù), 字符串,符號等等一樣造锅。一個函數(shù)并不必然有一個名字, 這和C++或者Java語言的情形很不相同撼唾。在這里函數(shù)自己代表自己。事實上它是一個指向代碼塊的指針, 附帶有一些其他信息(例如一組參數(shù)變量)备绽。只有在把函數(shù)賦予其他符號時, 它才具有了名字, 就像把一個數(shù)值或字符串賦予變量一樣的道理券坞。你可以用一個內(nèi)置的專門用于創(chuàng)建函數(shù)的函數(shù)來創(chuàng)建函數(shù),然后把它賦值給符號fn, 用偽碼來表示就是:
fn [a]
{
return *(a, 2);
}
這段代碼返回一個具有一個參數(shù)的函數(shù), 函數(shù)的功能是計算參數(shù)乘2的結(jié)果。這個函數(shù)還沒有名字, 你可以把此函數(shù)賦值給別的符號:
set(times-two, fn [a] {return *(a, 2)})
我們現(xiàn)在可以這樣調(diào)用這個函數(shù):
time-two(5) // 返回10
我們先跳過符號和函數(shù), 講一講表肺素。什么是表? 你也許已經(jīng)聽過好多相關(guān)的說法恨锚。表, 一言以蔽之, 就是把類似XML那樣的數(shù)據(jù)塊, 用s表達(dá)式來表示。表用一對括號括住, 表中元素以空格分隔, 表可以嵌套倍靡。例如(這回我們用真正的Lisp語法, 注意用分號表示注釋):
() ; 空表
(1) ; 含一個元素的表
(1 "test") ; 兩元素表, 一個元素是整數(shù)1, 另一個是字符串
(test "hello") ; 兩元素表, 一個元素是符號, 另一個是字符串
(test (1 2) "hello") ; 三元素表, 一個符號test, 一個含有兩個元素1和2的
; 表, 最后一個元素是字符串
當(dāng)Lisp系統(tǒng)遇到這樣的表時, 它所做的, 和Ant處理XML數(shù)據(jù)所做的, 非常相似, 那就是試圖執(zhí)行它們猴伶。其實, Lisp源碼就是特定的一種表, 好比Ant源碼是一種特定的XML一樣。Lisp執(zhí)行表的順序是這樣的, 表的第一個元素當(dāng)作函數(shù), 其他元素當(dāng)作函數(shù)的參數(shù)塌西。如果其中某個參數(shù)也是表, 那就按照同樣的原則對這個表求值, 結(jié)果再傳遞給最初的函數(shù)作為參數(shù)他挎。這就是基本原則。我們看一下真正的代碼:
(* 3 4) ; 相當(dāng)于前面列舉過的偽碼*(3,4), 即計算3乘4
(times-two 5) ; 返回10, times-two按照前面的定義是求參數(shù)的2倍
(3 4) ; 錯誤, 3不是函數(shù)
(time-two) ; 錯誤, times-two要求一個參數(shù)
(times-two 3 4) ; 錯誤, times-two只要求一個參數(shù)
(set + -) ; 把減法函數(shù)賦予符號+
(+ 5 4) ; 依據(jù)上一句的結(jié)果, 此時+表示減法, 所以返回1
(* 3 (+ 2 2)) ; 2+2的結(jié)果是4, 再乘3, 結(jié)果是12
上述的例子中, 所有的表都是當(dāng)作代碼來處理的捡需。怎樣把表當(dāng)作數(shù)據(jù)來處理呢? 同樣的,設(shè)想一下, Ant是把XML數(shù)據(jù)當(dāng)作自己的參數(shù)办桨。在Lisp中, 我們給表加一個前綴'來表示數(shù)據(jù)。
(set test '(1 2)) ; test的值為兩元素表
(set test (1 2)) ; 錯誤, 1不是函數(shù)
(set test '(* 3 4)) ; test的值是三元素表, 三個元素分別是*, 3, 4
我們可以用一個內(nèi)置的函數(shù)head來返回表的第一個元素, tail函數(shù)來返回剩余元素組成的表站辉。
(head '(* 3 4)) ; 返回符號*
(tail '(* 3 4)) ; 返回表(3 4)
(head (tal '(* 3 4))) ; 返回3
(head test) ; 返回*
你可以把Lisp的內(nèi)置函數(shù)想像成Ant的任務(wù)呢撞。差別在于, 我們不用在另外的語言中擴展Lisp(雖然完全可以做得到), 我們可以用Lisp自己來擴展自己, 就像上面舉的times-two函數(shù)的例子。Lisp的內(nèi)置函數(shù)集十分精簡, 只包含了十分必要的部分饰剥。剩下的函數(shù)都是作為標(biāo)準(zhǔn)庫來實現(xiàn)的殊霞。
Lisp宏
我們已經(jīng)看到, 元編程在一個類似jsp的模板引擎方面的應(yīng)用。我們通過簡單的字符串處理來生成代碼汰蓉。但是我們可以做的更好绷蹲。我們先提一個問題, 怎樣寫一個工具, 通過查找目錄結(jié)構(gòu)中的源文件來自動生成Ant腳本。
用字符串處理的方式生成Ant腳本是一種簡單的方式顾孽。當(dāng)然, 還有一種更加抽象, 表達(dá)能力更強, 擴展性更好的方式, 就是利用XML庫在內(nèi)存中直接生成XML節(jié)點, 這樣的話內(nèi)存中的節(jié)點就可以自動序列化成為字符串祝钢。不僅如此, 我們的工具還可以分析這些節(jié)點, 對已有的XML文件做變換。通過直接處理XML節(jié)點若厚。我們可以超越字符串處理, 使用更高層次的概念, 因此我們的工作就會做的更快更好太颤。
我們當(dāng)然可以直接用Ant自身來處理XML變換和制作代碼生成工具№锷颍或者我們也可以用Lisp來做這項工作龄章。正像我們以前所知的, 表是Lisp內(nèi)置的數(shù)據(jù)結(jié)構(gòu), Lisp含有大量的工具來快速有效的操作表(head和tail是最簡單的兩個)。而且, 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è)我們要寫一個任務(wù)表的管理程序, 把任務(wù)表數(shù)據(jù)存到一組文件里, 當(dāng)程序啟動時, 從文件讀取這些數(shù)據(jù)并顯示給用戶肃晚。在別的語言里(比如說Java), 這個任務(wù)該怎么做? 我們會解析XML文件, 從中得出任務(wù)表數(shù)據(jù), 然后寫代碼遍歷XML樹, 再轉(zhuǎn)換為Java的數(shù)據(jù)結(jié)構(gòu)(老實講, 在Java里解析XML真不是件輕松的事情), 最后再把數(shù)據(jù)展示給用戶∶現(xiàn)在如果用Lisp, 該怎么做?
假定要用同樣思路的化, 我們大概會用Lisp庫來解析XML。XML對我們來說就是一個Lisp的表(s表達(dá)式), 我們可以遍歷這個表, 然后把相關(guān)數(shù)據(jù)提交給用戶关串∨±龋可是, 既然我們用Lisp, 就根本沒有必要再用XML格式保存數(shù)據(jù), 直接用s表達(dá)式就好了, 這樣就沒有必要做轉(zhuǎn)換了监徘。我們也用不著專門的解析庫, Lisp可以直接在內(nèi)存里處理s表達(dá)式。注意, Lisp編譯器和.net編譯器一樣, 對Lisp程序來說, 在運行時總是隨時可用的吧碾。
但是還有更好的辦法凰盔。我們甚至不用寫表達(dá)式來存儲數(shù)據(jù), 我們可以寫宏, 把數(shù)據(jù)當(dāng)作代碼來處理。那該怎么做呢? 真的簡單倦春』Ь矗回想一下, Lisp的函數(shù)調(diào)用格式:
(function-name arg1 arg2 arg3)
其中每個參數(shù)都是s表達(dá)式, 求值以后, 傳遞給函數(shù)。如果我們用(+ 4 5)來代替arg1,那么, 程序會先求出結(jié)果, 就是9, 然后把9傳遞給函數(shù)睁本。宏的工作方式和函數(shù)類似尿庐。主要的差別是, 宏的參數(shù)在代入時不求值。
(macro-name (+ 4 5))
這里, (+ 4 5)作為一個表傳遞給宏, 然后宏就可以任意處理這個表, 當(dāng)然也可以對它求值呢堰。宏的返回值是一個表, 然后有程序作為代碼來執(zhí)行抄瑟。宏所占的位置, 就被替換為這個結(jié)果代碼。我們可以定義一個宏把數(shù)據(jù)替換為任意代碼, 比方說, 替換為顯示數(shù)據(jù)給用戶的代碼枉疼。
這和元編程, 以及我們要做的任務(wù)表程序有什么關(guān)系呢? 實際上, 編譯器會替我們工作,調(diào)用相應(yīng)的宏锐借。我們所要做的, 僅僅是創(chuàng)建一個把數(shù)據(jù)轉(zhuǎn)換為適當(dāng)代碼的宏。
例如, 上面曾經(jīng)將過的C的求三次方的宏, 用Lisp來寫是這樣子:
(defmacro triple (x)
`(+ ~x ~x ~x))
(譯注: 在Common Lisp中, 此處的單引號應(yīng)當(dāng)是反單引號, 意思是對表不求值, 但可以對表中某元素求值, 記號~表示對元素x求值, 這個求值記號在Common Lisp中應(yīng)當(dāng)是逗號往衷。反單引號和單引號的區(qū)別是, 單引號標(biāo)識的表, 其中的元素都不求值钞翔。這里作者所用的記號是自己發(fā)明的一種Lisp方言Blaise, 和common lisp略有不同, 事實上, 發(fā)明方言是lisp高手獨有的樂趣, 很多狂熱分子都熱衷這樣做。比如Paul Graham就發(fā)明了ARC, 許多記號比傳統(tǒng)的Lisp簡潔得多, 顯得比較現(xiàn)代)
單引號的用處是禁止對表求值席舍。每次程序中出現(xiàn)triple的時候,
(triple 4)
都會被替換成:
(+ 4 4 4)
我們可以為任務(wù)表程序?qū)懸粋€宏, 把任務(wù)數(shù)據(jù)轉(zhuǎn)換為可執(zhí)行碼, 然后執(zhí)行布轿。假定我們的輸出是在控制臺:
(defmacro item (priority note)
`(block
(print stdout tab "Prority: " ~(head (tail priority)) endl)
(print stdout tab "Note: " ~note endl endl)))
我們創(chuàng)造了一個非常小的有限的語言來管理嵌在Lisp中的任務(wù)表。這個語言只用來解決特定領(lǐng)域的問題, 通常稱之為DSLs(特定領(lǐng)域語言, 或?qū)S妙I(lǐng)域語言)来颤。
特定領(lǐng)域語言
本文談到了兩個特定領(lǐng)域語言, 一個是Ant, 處理軟件構(gòu)造汰扭。一個是沒起名字的, 用于處理任務(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ù)問題, 等等笆包。我們可以把這些疊放在一起, 造出一個語言, 專門解決基于網(wǎng)絡(luò)的貿(mào)易程序, 既有網(wǎng)絡(luò)語言的優(yōu)勢, 又有貿(mào)易語言的好處。每天我們都會收獲這種方法帶給我們的益處, 遠(yuǎn)遠(yuǎn)超過Ant所能給予我們的略荡。
用DSL解決問題, 做出的程序精簡, 易于維護(hù), 富有彈性庵佣。在Java里面, 我們可以用類來處理問題。這兩種方法的差別在于, Lisp使我們達(dá)到了一個更高層次的抽象, 我們不再受語言解析器本身的限制, 比較一下用Java庫直接寫的構(gòu)造腳本和用Ant寫的構(gòu)造腳本其間的差別汛兜。同樣的, 比較一下你以前所做的工作, 你就會明白Lisp帶來的好處巴粪。
接下來
學(xué)習(xí)Lisp就像戰(zhàn)爭中爭奪山頭。盡管在電腦科學(xué)領(lǐng)域, Lisp已經(jīng)算是一門古老的語言, 直到現(xiàn)在仍然很少有人真的明白該怎樣給初學(xué)者講授Lisp。盡管Lisp老手們盡了很大努力,今天新手學(xué)習(xí)Lisp仍然是困難重重肛根。好在現(xiàn)在事情正在發(fā)生變化, Lisp的資源正在迅速增加, 隨著時間推移, Lisp將會越來越受關(guān)注辫塌。
Lisp使人超越平庸, 走到前沿。學(xué)會Lisp意味著你能找到更好的工作, 因為聰明的雇主會被你與眾不同的洞察力所打動派哲。學(xué)會Lisp也可能意味著明天你可能會被解雇, 因為你總是強調(diào), 如果公司所有軟件都用Lisp寫, 公司將會如何卓越, 而這些話你的同事會聽煩的臼氨。Lisp值得努力學(xué)習(xí)嗎? 那些已經(jīng)學(xué)會Lisp的人都說值得, 當(dāng)然, 這取決于你的判斷。
你的看法呢?
這篇文章寫寫停停, 用了幾個月才最終完成狮辽。如果你覺得有趣, 或者有什么問題, 意見或建議, 請給我發(fā)郵件coffeemug@gmail.com, 我會很高興收到你的反饋。