光劍評注:其實(shí)迟杂,說了這么多廢話刽沾,無非就是: 一切皆是映射。不管是嵌套 XML排拷,還是 Lisp 嵌套括號侧漓,還是 XXX 的 Map 數(shù)據(jù)結(jié)構(gòu),一切都是樹形結(jié)構(gòu)——映射监氢。

Lisp的本質(zhì)(The Nature of Lisp)

http://www.defmacro.org/ramblings/lisp.html

簡介

最初在web的某些角落偶然看到有人贊美Lisp時(shí), 我那時(shí)已經(jīng)是一個(gè)頗有經(jīng)驗(yàn)的程序員布蔗。

在我的履歷上, 掌握的語言范圍相當(dāng)廣泛, 象C++, Java, C#主流語言等等都不在話下, 我覺得我差不多知道所有的有關(guān)編程語言的事情。對待編程語言的問題上, 我覺得自己不太會遇到什么大問題浪腐。其實(shí)我大錯(cuò)特錯(cuò)了纵揍。

我試著學(xué)了一下Lisp, 結(jié)果馬上就撞了墻。我被那些范例代碼嚇壞了牛欢。我想很多初次接觸Lisp語言的人, 一定也有過類似的感受骡男。Lisp的語法太次了。一個(gè)語言的發(fā)明人, 居然不肯用心弄出一套漂亮的語法, 那誰還會愿意學(xué)它傍睹。反正, 我是確確實(shí)實(shí)被那些難看的無數(shù)
的括號搞蒙了隔盛。

回過神來之后, 我和Lisp社區(qū)的那伙人交談, 訴說我的沮喪心情。結(jié)果, 立馬就有一大套理論砸過來, 這套理論在Lisp社區(qū)處處可見, 幾成慣例拾稳。

比如說:

Lisp的括號只是表面現(xiàn)象;
Lisp的代碼和數(shù)據(jù)的表達(dá)方式?jīng)]有差別, 而且比XML語法高明許多, 所以有無窮的好處;
Lisp有強(qiáng)大無比的元語言能力, 程序員可以寫出自我維護(hù)的代碼; Lisp可以創(chuàng)造出針對特定應(yīng)用的語言子集DSL;
Lisp的運(yùn)行時(shí)和編譯時(shí)沒有明確的分界;
等等, 等等, 等等吮炕。

這么長的贊美詞雖然看起來相當(dāng)動人, 不過對我毫無意義。沒人能給我演示這些東西是如何應(yīng)用的, 因?yàn)檫@些東西一般來說只有在大型系統(tǒng)才會用到访得。我爭辯說, 這些東西傳統(tǒng)語言一樣辦得到龙亲。在和別人爭論了數(shù)個(gè)小時(shí)之后, 我最終還是放棄了學(xué)Lisp的念頭。為什么要花費(fèi)幾個(gè)月的時(shí)間學(xué)習(xí)語法這么難看的語言呢? 這種語言的概念這么晦澀, 又沒什么好懂的例子悍抑。也許這語言不是該我這樣的人學(xué)的鳄炉。

幾個(gè)月來, 我承受著這些Lisp辯護(hù)士對我心靈的重壓。我一度陷入了困惑搜骡。我認(rèn)識一些絕頂聰明的人, 我對他們相當(dāng)尊敬, 我看到他們對Lisp的贊美達(dá)到了宗教般的高度拂盯。這就是說, Lisp中一定有某種神秘的東西存在, 我不能忍受自己對此的無知, 好奇心和求知欲最
終不可遏制。我于是咬緊牙關(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é)會Lisp之后, 你會擁有深刻的體驗(yàn)空凸。就算你平常并不用Lisp編程, 它也會使你成為更加優(yōu)秀的程序員"嚎花。

過去, 我根本不懂這些話的含義, 我也不相信這是真的⊙街蓿可是現(xiàn)在我懂得了紊选。這些話蘊(yùn)含的真理遠(yuǎn)遠(yuǎn)超過我過去的想像。我內(nèi)心體會到一種神圣的情感, 一瞬間的頓悟, 幾乎使我對電腦科學(xué)的觀念發(fā)生了根本的改變道逗。

頓悟的那一刻, 我成了Lisp的崇拜者丛楚。我體驗(yàn)到了宗教大師的感受: 一定要把我的知識傳布開來, 至少要讓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)會? 不是, 沒有任何絕對艱深的東西。因?yàn)槲夷芘? 我相信其他人也一定能拢操。那么問題出在那里? 后來我終于找到了答案锦亦。我的結(jié)論就是, 凡是教人學(xué)高級概念, 一定要
從他已經(jīng)懂得的東西開始。如果學(xué)習(xí)過程很有趣, 學(xué)習(xí)的內(nèi)容表達(dá)得很恰當(dāng), 新概念就會變得相當(dāng)直觀令境。這就是我的答案杠园。

所謂元編程, 所謂數(shù)據(jù)和代碼形式合一, 所謂自修改代碼, 所謂特定應(yīng)用的子語言, 所有這些概念根本就是同族概念, 彼此互為解釋, 肯定越講越不明白。還是從實(shí)際的例子出發(fā)最有用舔庶。

我把我的想法說給Lisp程序員聽, 遭到了他們的反對抛蚁。"這些東西本身當(dāng)然不可能用熟悉的知識來解釋, 這些概念完全與眾不同, 你不可能在別人已有的經(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)有趣。

光劍評注:其實(shí)栓始,說了這么多廢話务冕,無非就是: 一切皆是映射。不管是嵌套 XML混滔,還是 Lisp 嵌套括號,還是 XXX 的 Map 數(shù)據(jù)結(jié)構(gòu)饿这,一切都是樹形結(jié)構(gòu)——映射奢米。

XML和Lisp的概念有著驚人的相似之處。XML是我們通向理解Lisp的橋梁巍扛。好吧, 我們且把
XML當(dāng)作活馬醫(yī)。讓我們拿好手杖, 對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í)會發(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來表示, 反之
亦然郑什。希望你能懂得這一點(diǎn), 這對下面的內(nèi)容極其重要府喳。

再進(jìn)一步。還有什么類型的數(shù)據(jù)也常用樹來表示? 無疑列表(list)也是一種蘑拯。上過編譯課
吧? 還模模糊糊記得一點(diǎn)吧? 源代碼在解析之后也是用樹結(jié)構(gòu)來存放的, 任何編譯程序都
會把源代碼解析成一棵抽象語法樹, 這樣的表示法很恰當(dāng), 因?yàn)樵创a就是層次結(jié)構(gòu)的:
函數(shù)包含參數(shù)和代碼塊, 代碼快包含表達(dá)式和語句, 語句包含變量和運(yùn)算符等等钝满。

我們已經(jīng)知道, 任何樹結(jié)構(gòu)都可以輕而易舉的寫成XML, 而任何代碼都會解析成樹, 因此,
任何代碼都可以轉(zhuǎn)換成XML, 對不對? 我舉個(gè)例子, 請看下面的函數(shù):

int add(int arg1, int arg2)
{
return arg1+arg2;
}

能把這個(gè)函數(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>

這個(gè)例子非常簡單, 用哪種語言來做都不會有太大問題申窘。我們可以把任何程序碼轉(zhuǎn)成XML,
也可以把XML轉(zhuǎn)回到原來的程序碼弯蚜。我們可以寫一個(gè)轉(zhuǎn)換器, 把Java代碼轉(zhuǎn)成XML, 另一個(gè)
轉(zhuǎn)換器把XML轉(zhuǎn)回到Java。一樣的道理, 這種手段也可以用來對付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作為源代碼的通用存儲方式, 其實(shí)我們能夠產(chǎn)生一整套
使用統(tǒng)一語法的程序語言, 也能寫出轉(zhuǎn)換器, 把已有代碼轉(zhuǎn)換成XML格式帽氓。如果真的采納
這種辦法, 各種語言的編譯器就用不著自己寫語法解析了, 它們可以直接用XML的語法解
析來直接生成抽象語法樹。

說到這里你該問了, 我們研究了這半天XML, 這和Lisp有什么關(guān)系呢? 畢竟XML出來之時(shí),
Lisp早已經(jīng)問世三十年了俩块。這里我可以保證, 你馬上就會明白黎休。不過在繼續(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ù),
對吧? 一旦經(jīng)過解析, 它就可以拿給編譯器編譯執(zhí)行署照。我們可以輕而易舉寫出這個(gè)XML
代碼解釋器, 并且直接運(yùn)行它祸泪。或者我們也可以把它譯成Java或C++代碼, 然后再編譯運(yùn)
行建芙。所以說, 它也是代碼没隘。

我們說到那里了? 不錯(cuò), 我們已經(jīng)發(fā)現(xiàn)了一個(gè)有趣的關(guān)鍵之點(diǎn)。過去被認(rèn)為很難解的概念
已經(jīng)非常直觀非常簡單的顯現(xiàn)出來禁荸。代碼也是數(shù)據(jù), 并且從來都是如此右蒲。這聽起來瘋瘋癲
癲的, 實(shí)際上卻是必然之事。我許諾過會以一種全新的方式來解釋Lisp, 我要重申我的許
諾赶熟。但是我們此刻還沒有到預(yù)定的地方, 所以還是先繼續(xù)上邊的討論瑰妄。

剛才我說過, 我們可以非常簡單地實(shí)現(xiàn)XML版的add函數(shù)解釋器, 這聽起來好像不過是說說
而已。誰真的會動手做一下呢? 未必有多少人會認(rèn)真對待這件事映砖。隨便說說, 并不打算真
的去做, 這樣的事情你在生活中恐怕也遇到吧间坐。你明白我這樣說的意思吧, 我說的有沒有
打動你? 有哇, 那好, 我們繼續(xù)。

重新審視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è)簡單
但是十分有用的工具, 這個(gè)工具巧妙地利用了Java屬性文件中的信息來構(gòu)造工程, 現(xiàn)在
James可以寫makefile的替代品, 它能起到相同的作用, 而形式更加優(yōu)美, 也不用擔(dān)心有
makefile那樣可恨的空格問題。這個(gè)工具能夠自動解釋屬性文件, 然后采取正確的動作來
編譯工程溯警。真是簡單而優(yōu)美趣苏。

(作者注: 我不認(rèn)識James, James也不認(rèn)識我, 這個(gè)故事是根據(jù)網(wǎng)上關(guān)于Ant歷史的帖子
虛構(gòu)的)

使用Ant構(gòu)造Tomcat之后幾個(gè)月, 他越來越感到Java的屬性文件不足以表達(dá)復(fù)雜的構(gòu)造指
令。文件需要檢出, 拷貝, 編譯, 發(fā)到另外一臺機(jī)器, 進(jìn)行單元測試梯轻。要是出錯(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是怎樣工作的曹宴?原理非常簡單搂橙。Ant把包含有構(gòu)造命令的XML文件(算代碼還是算數(shù)據(jù),
你自己想吧),交給一個(gè)Java程序來解析每一個(gè)元素笛坦,實(shí)際情況比我說的還要簡單得多区转。
一個(gè)簡單的XML指令會導(dǎo)致具有相同名字的Java類裝入苔巨,并執(zhí)行其代碼。

<copy todir="../new/dir">
    <fileset dir="src_dir" />
</copy>

這段文字的含義是把源目錄復(fù)制到目標(biāo)目錄废离,Ant會找到一個(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)的代碼蔬顾。過程非常簡單。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)站上去找答
案了, 不會找到有價(jià)值的東西。至少對我們這個(gè)論題來說是如此活翩。我們還是繼續(xù)下一步討
論吧烹骨。我們答案就在那里。

為什么是XML

有時(shí)候正確的決策并非完全出于深思熟慮材泄。我不知道James選擇XML是否出于深思熟慮沮焕。也
許僅僅是個(gè)下意識的決定。至少從James在Ant網(wǎng)站上發(fā)表的文章看起來, 他所說的理由完
全是似是而非拉宗。他的主要理由是移植性和擴(kuò)展性, 在Ant案例上, 我看不出這兩條有什么
幫助峦树。使用XML而不是Java代碼, 到底有什么好處? 為什么不寫一組Java類, 提供api來滿
足基本任務(wù)(拷貝目錄, 編譯等等), 然后在Java里直接調(diào)用這些代碼? 這樣做仍然可以保
證移植性, 擴(kuò)展性也是毫無疑問的。而且語法也更為熟悉, 看著順眼旦事。那為什么要用 XML
呢? 有什么更好的理由嗎?

有的魁巩。雖然我不確定James是否確實(shí)意識到了。在語義的可構(gòu)造性方面, XML的彈性是Java
望塵莫及的姐浮。我不想用高深莫測的名詞來嚇唬你, 其中的道理相當(dāng)簡單, 解釋起來并不費(fèi)
很多功夫谷遂。好, 做好預(yù)備動作, 我們馬上就要朝向頓悟的時(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è)很相似, 只是稍微長一點(diǎn)卖鲤。差別在那里? 差別在于XML構(gòu)造了
一個(gè)特殊的copy動詞, 如果我們硬要用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, 大
概就會得到上面這段事實(shí)上不可以執(zhí)行的代碼。因?yàn)镴ava的技術(shù)規(guī)范是定死的, 我們沒有
辦法在程序里改變它蛋逾。我們可以增加包, 增加類, 增加方法, 但是我們沒辦法增加算符,
而對于XML, 我們顯然可以任由自己增加這樣的東西白对。對于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元素, 生成抽象語法樹, 由此生成算符, 所以, 我們可以任意引入任何算符。

對于復(fù)雜的算符來說, 這樣做的好處顯而易見。比如, 用特定的算符來做檢出源碼, 編譯
文件, 單元測試, 發(fā)送郵件等任務(wù), 想想看有多么美妙条摸。對于特定的題目, 比如說構(gòu)造軟
件項(xiàng)目, 這些算符的使用可以大幅減低少代碼的數(shù)量悦污。增加代碼的清晰程度和可重用性。
解釋性的XML可以很容易的達(dá)到這個(gè)目標(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算符就是一個(gè)例子踏枣。它用的還是傳統(tǒng)的作法:
C#的設(shè)計(jì)者修改抽象語法樹, 然后增加對應(yīng)的實(shí)現(xiàn)。如果程序員自己也能修改抽象語法樹
該有多好! 那樣我們就可以構(gòu)造用于特定問題的子語言(比如說就像Ant這種用于構(gòu)造項(xiàng)目
的語言), 你能想到別的例子嗎? 再思考一下這個(gè)概念钙蒙。不過也不必思考太甚, 我們待會
還會回到這個(gè)題目茵瀑。那時(shí)候就會更加清晰。

離Lisp越來越近

我們先把算符的事情放一放, 考慮一下Ant設(shè)計(jì)局限之外的東西躬厌。我早先說過, Ant可以通
過寫Java類來擴(kuò)展马昨。Ant解析器會根據(jù)名字來匹配XML元素和Java類, 一旦找到匹配, 就執(zhí)
行相應(yīng)任務(wù)。為什么不用Ant自己來擴(kuò)展Ant呢? 畢竟核心任務(wù)要包含很多傳統(tǒng)語言的結(jié)構(gòu)
(例如"if"), 如果Ant自身就能提供構(gòu)造任務(wù)的能力(而不是依賴java類), 我們就可以得
到更高的移植性扛施。我們將會依賴一組核心任務(wù)(如果你原意, 也不妨把它稱作標(biāo)準(zhǔn)庫), 而
不用管有沒有Java 環(huán)境了鸿捧。這組核心任務(wù)可以用任何方式來實(shí)現(xiàn), 而其他任務(wù)建筑在這
組核心任務(wù)之上, 那樣的話, Ant就會成為通用的, 可擴(kuò)展的, 基于XML的編程語言「碓考慮
下面這種代碼的可能性:

<task name="Test">
    <echo message="Hello World" />
</task>
<Test />

如果XML支持"task"的創(chuàng)建, 上面這段代碼就會輸出"Hello World!". 實(shí)際上, 我們可以
用Java寫個(gè)"task"任務(wù), 然后用Ant-XML來擴(kuò)展它匙奴。Ant可以在簡單原語的基礎(chǔ)上寫出更復(fù)
雜的原語, 就像其他編程語言常用的作法一樣。這也就是我們一開始提到的基于XML的編
程語言妄荔。這樣做用處不大(你知道為甚么嗎?), 但是真的很酷泼菌。

再看一回我們剛才說的Task任務(wù)。祝賀你呀, 你在看Lisp代碼!!! 我說什么? 一點(diǎn)都不像
Lisp嗎? 沒關(guān)系, 我們再給它收拾一下懦冰。

比XML更好

前面一節(jié)說過, Ant自我擴(kuò)展沒什么大用, 原因在于XML很煩瑣。對于數(shù)據(jù)來說, 這個(gè)問題
還不太大, 但如果代碼很煩瑣的話, 光是打字上的麻煩就足以抵消它的好處谣沸。你寫過Ant
的腳本嗎? 我寫過, 當(dāng)腳本達(dá)到一定復(fù)雜度的時(shí)候, XML非常讓人厭煩刷钢。想想看吧, 為了
寫結(jié)束標(biāo)簽, 每個(gè)詞都得打兩遍, 不發(fā)瘋算好的!

為了解決這個(gè)問題, 我們應(yīng)當(dāng)簡化寫法。須知, XML僅僅是一種表達(dá)層次化數(shù)據(jù)的方式乳附。
我們并不是一定要使用尖括號才能得到樹的序列化結(jié)果内地。我們完全可以采用其他的格式。
其中的一種(剛好就是Lisp所采用的)格式, 叫做s表達(dá)式赋除。s表達(dá)式要做的和XML一樣, 但
它的好處是寫法更簡單, 簡單的寫法更適合代碼輸入阱缓。后面我會詳細(xì)講s表達(dá)式。這之前
我要清理一下XML的東西举农【U耄考慮一下關(guān)于拷貝文件的例子:

<copy toDir="../new/dir">
    <fileset dir="src_dir">
</copy>

想想看在內(nèi)存里面, 這段代碼的解析樹在內(nèi)存會是什么樣子? 會有一個(gè)"copy"節(jié)點(diǎn), 其下
有一個(gè) "fileset"節(jié)點(diǎn), 但是屬性在哪里呢? 它怎樣表達(dá)呢? 如果你以前用過XML, 并且
弄不清楚該用元素還是該用屬性, 你不用感到孤單, 別人一樣糊涂著呢。沒人真的搞得清
楚。這個(gè)選擇與其說是基于技術(shù)的理由, 還不如說是閉著眼瞎摸航背。從概念上來講, 屬性也
是一種元素, 任何屬性能做的, 元素一樣做得到喉悴。XML引入屬性的理由, 其實(shí)就是為了讓
XML寫法不那么冗長。比如我們看個(gè)例子:

<copy>
    <toDir>../new/dir</toDir>
    <fileset>
        <dir>src_dir</dir>
    </fileset>
</copy>

兩下比較, 內(nèi)容的信息量完全一樣, 用屬性可以減少打字?jǐn)?shù)量玖媚。如果XML沒有屬性的話,
光是打字就夠把人搞瘋掉箕肃。

說完了屬性的問題, 我們再來看一看s表達(dá)式。之所以繞這么個(gè)彎, 是因?yàn)閟表達(dá)式?jīng)]有屬
性的概念今魔。因?yàn)閟表達(dá)式非常簡練, 根本沒有必要引入屬性勺像。我們在把XML轉(zhuǎn)換成s表達(dá)式
的時(shí)候, 心里應(yīng)該記住這一點(diǎn)〈砩看個(gè)例子, 上面的代碼譯成s表達(dá)式是這樣的:

(copy 
    (todir "../new/dir")
    (fileset (dir "src_dir")))

仔細(xì)看看這個(gè)例子, 差別在哪里? 尖括號改成了圓括號, 每個(gè)元素原來是有一對括號標(biāo)記
包圍的, 現(xiàn)在取消了后一個(gè)(就是帶斜杠的那個(gè))括號標(biāo)記吟宦。表示元素的結(jié)束只需要一個(gè)")"
就可以了。不錯(cuò), 差別就是這些问词。這兩種表達(dá)方式的轉(zhuǎn)換, 非常自然, 也非常簡單督函。s表
達(dá)式打起字來, 也省事得多。第一次看s表達(dá)式(Lisp)時(shí), 括號很煩人是吧? 現(xiàn)在我們明
白了背后的道理, 一下子就變得容易多了激挪。至少, 比XML要好的多辰狡。用s表達(dá)式寫代碼, 不
單是實(shí)用, 而且也很讓人愉快。s表達(dá)式具有XML的一切好處, 這些好處是我們剛剛探討過
的÷⒎郑現(xiàn)在我們看看更加Lisp風(fēng)格的task例子:

(task (name "Test")
    (echo (message "Hellow World!")))
(Test)

用Lisp的行話來講, s表達(dá)式稱為表(list)宛篇。對于上面的例子, 如果我們寫的時(shí)候不加換
行, 用逗號來代替空格, 那么這個(gè)表達(dá)式看起來就非常像一個(gè)元素列表, 其中又嵌套著其
他標(biāo)記。

(task, (name, "test"), (echo, (message, "Hello World!")))

XML自然也可以用這樣的風(fēng)格來寫薄湿。當(dāng)然上面這句并不是一般意義上的元素表叫倍。它實(shí)際上
是一個(gè)樹。這和XML的作用是一樣的豺瘤。稱它為列表, 希望你不會感到迷惑, 因?yàn)榍短妆砗?br> 樹實(shí)際上是一碼事吆倦。Lisp的字面意思就是表處理(list processing), 其實(shí)也可以稱為樹
處理, 這和處理XML節(jié)點(diǎn)沒有什么不同。

經(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)于元編程的事情, 也探討過專門寫代碼的代碼须妻。理解這問題
有一定難度, 因?yàn)橄嚓P(guān)討論文章所使用的編程語言, 都是你們不熟悉的。但是如果只論概
念的話, 就相對要簡單一些泛领。我相信, 如果以C語言做例子來討論元編程, 理解起來一定
會容易得多荒吏。好, 我們接著看。

一個(gè)問題是, 為什么要用代碼來寫代碼呢? 在實(shí)際的編程中, 怎樣做到這一點(diǎn)呢? 到底元
編程是什么意思? 你大概已經(jīng)聽說過這些問題的答案, 但是并不懂得其中緣由渊鞋。為了揭示
背后的真理, 我們來看一下一個(gè)簡單的數(shù)據(jù)庫查詢問題绰更。這種題目我們都做過瞧挤。比方說,
直接在程序碼里到處寫SQL語句來修改表(table)里的數(shù)據(jù), 寫多了就非常煩人。即便用
C#3.0的LINQ, 仍然不減其痛苦动知。寫一個(gè)完整的SQL查詢(盡管語法很優(yōu)美)來修改某人的地
址, 或者查找某人的名字, 絕對是件令程序員倍感乏味的事情, 那么我們該怎樣來解決這
個(gè)問題? 答案就是: 使用數(shù)據(jù)訪問層皿伺。

概念挺簡單, 其要點(diǎn)是把數(shù)據(jù)訪問的內(nèi)容(至少是那些比較瑣碎的部分)抽象出來, 用類來
映射數(shù)據(jù)庫的表, 然后用訪問對象屬性訪問器(accessor)的辦法來間接實(shí)現(xiàn)查詢。這樣就
極大地簡化了開發(fā)工作量盒粮。我們用訪問對象的方法(或者屬性賦值, 這要視你選用的語言
而定)來代替寫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ù)定義的或者用戶定制的模板來
自動編寫代碼讼油。

如果你用過這種工具, 你肯定會對它的神奇效果深為折服。往往只需要鼠標(biāo)點(diǎn)擊數(shù)次, 就
可以連接到數(shù)據(jù)庫, 產(chǎn)生數(shù)據(jù)訪問源碼, 然后把文件加入到你的工程里面, 十幾分鐘的工
作, 按照往常手工方式來作的話, 也許需要數(shù)百個(gè)小時(shí)人工(man-hours)才能完成呢簸“ǎ可是,
如果你的數(shù)據(jù)庫結(jié)構(gòu)定義后來改變了怎么辦? 那樣的話, 你只需把這個(gè)過程重復(fù)一遍就可
以了。甚至有一些工具能自動完成這項(xiàng)變動工作根时。你只要把它作為工程構(gòu)造的一部分, 每
次編譯工程的時(shí)候, 數(shù)據(jù)庫部分也會自動地重新構(gòu)造瘦赫。這真的太棒了。你要做的事情基本
上減到了0蛤迎。如果數(shù)據(jù)庫結(jié)構(gòu)定義發(fā)生了改變, 并在編譯時(shí)自動更新了數(shù)據(jù)訪問層的代碼,
那么程序中任何使用過時(shí)的舊代碼的地方, 都會引發(fā)編譯錯(cuò)誤确虱。

數(shù)據(jù)訪問層是個(gè)很好的例子, 這樣的例子還有好多。從GUI樣板代碼, WEB代碼, COM和
CORBA存根, 以及MFC和ATL等等替裆。在這些地方, 都是有好多相似代碼多次重復(fù)校辩。既然這些
代碼有可能自動編寫, 而程序員時(shí)間又遠(yuǎn)遠(yuǎn)比CPU時(shí)間昂貴, 當(dāng)然就產(chǎn)生了好多工具來自
動生成樣板代碼。這些工具的本質(zhì)是什么呢? 它們實(shí)際上就是制造程序的程序辆童。它們有一
個(gè)神秘的名字, 叫做元編程宜咒。所謂元編程的本義, 就是如此。

元編程本來可以用到無數(shù)多的地方, 但實(shí)際上使用的次數(shù)卻沒有那么多把鉴。歸根結(jié)底, 我們
心里還是在盤算, 假設(shè)重復(fù)代碼用拷貝粘貼的話, 大概要重復(fù)6,7次, 對于這樣的工作量,
值得專門建立一套生成工具嗎? 當(dāng)然不值得故黑。數(shù)據(jù)訪問層和COM存根往往需要重用數(shù)百次,
甚至上千次, 所以用工具生成是最好的辦法。而那些僅僅是重復(fù)幾次十幾次的代碼, 是沒
有必要專門做工具的纸镊。不必要的時(shí)候也去開發(fā)代碼生成工具, 那就顯然過度估計(jì)了代碼生
成的好處倍阐。當(dāng)然, 如果創(chuàng)建這類工具足夠簡單的話, 還是應(yīng)當(dāng)盡量多用, 因?yàn)檫@樣做必然
會節(jié)省時(shí)間「沤現(xiàn)在來看一下有沒有合理的辦法來達(dá)到這個(gè)目的逗威。

現(xiàn)在, C預(yù)處理器要派上用場了。我們都用過C/C++的預(yù)處理器, 我們用它執(zhí)行簡單的編譯
指令, 來產(chǎn)生簡單的代碼變換(比方說, 設(shè)置調(diào)試代碼開關(guān)), 看一個(gè)例子:

#define triple(X) X+X+X

這一行的作用是什么? 這是一個(gè)簡單的預(yù)編譯指令, 它把程序中的triple(X)替換稱為
X+X+X岔冀。例如, 把所有的triple(5)都換成5+5+5, 然后再交給編譯器編譯凯旭。這就是一個(gè)簡
單的代碼生成的例子。要是C的預(yù)處理器再強(qiáng)大一點(diǎn), 要是能夠允許連接數(shù)據(jù)庫, 要是能
多一些其他簡單的機(jī)制, 我們就可以在我們程序的內(nèi)部開發(fā)自己的數(shù)據(jù)訪問層。下面這個(gè)
例子, 是一個(gè)假想的對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ù)表, 然后對每個(gè)表創(chuàng)建一個(gè)類, 只消幾行代碼就完成
了這個(gè)工作罐呼。這樣每次編譯工程的時(shí)候, 這些類都會根據(jù)數(shù)據(jù)庫的定義同步更新鞠柄。顯而易
見, 我們不費(fèi)吹灰之力就在程序內(nèi)部建立了一個(gè)完整的數(shù)據(jù)訪問層, 根本用不著任何外部
工具。當(dāng)然這種作法有一個(gè)缺點(diǎn), 那就是我們得學(xué)習(xí)一套新的"編譯時(shí)語言", 另一個(gè)缺點(diǎn)
就是根本不存在這么一個(gè)高級版的C預(yù)處理器嫉柴。需要做復(fù)雜代碼生成的時(shí)候, 這個(gè)語言(譯
者注: 這里指預(yù)處理指令, 即作者所說的"編譯時(shí)語言")本身也一定會變得相當(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è)簡單的指示字 , 這些指示字用來
區(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)會提示你輸入一個(gè)數(shù), 這個(gè)數(shù)在后面的循環(huán)中會用到登馒。而for循
環(huán)的代碼會被編譯匙握。假定你在編譯時(shí)輸入5, for循環(huán)的代碼將會是:

for(int i=0;i<5; i++)
{
    cout<<"hello"<<endl;
}

又簡單又有效率, 也不需要另外的預(yù)處理語言。我們可以在編譯時(shí)就充分發(fā)揮宿主語言(
此處是C/C++)的強(qiáng)大能力, 我們可以很容易地在編譯時(shí)連接數(shù)據(jù)庫, 建立數(shù)據(jù)訪問層, 就
像JSP或者ASP創(chuàng)建網(wǎng)頁那樣陈轿。我們也用不著專門的窗口工具來另外建立工程圈纺。我們可以在
代碼中立即加入必要的工具。我們也用不著顧慮建立這種工具是不是值得, 因?yàn)檫@太容易
了, 太簡單了麦射。這樣子不知可以節(jié)省多少時(shí)間啊蛾娶。

你好, 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這樣的語言大體相同茫叭。真正有意思的三種類型是
符號(symbol), 表和函數(shù)。這一章的剩余部分, 我都會用來介紹這幾種類型, 還要介紹
Lisp環(huán)境是怎樣編譯和運(yùn)行源碼的半等。這個(gè)過程用Lisp的術(shù)語來說通常叫做求值揍愁。通讀這一
節(jié)內(nèi)容, 對于透徹理解元編程的真正潛力, 以及代碼和數(shù)據(jù)的同一性, 和面向領(lǐng)域語言的
觀念, 都極其重要。萬勿等閑視之杀饵。我會盡量講得生動有趣一些, 也希望你能獲得一些
啟發(fā)莽囤。那好, 我們先講符號。

大體上, 符號相當(dāng)于C++或Java語言中的標(biāo)志符, 它的名字可以用來訪問變量值(例如
currentTime, arrayCount, n, 等等), 差別在于, Lisp中的符號更加基本切距。在C++或
Java里面, 變量名只能用字母和下劃線的組合, 而Lisp的符號則非常有包容性, 比如, 加
號(+)就是一個(gè)合法的符號, 其他的像-, =, hello-world, *等等都可以是符號名朽缎。符號
名的命名規(guī)則可以在網(wǎng)上查到送淆。你可以給這些符號任意賦值, 我們這里先用偽碼來說明這
一點(diǎn)振坚。假定函數(shù)set是給變量賦值(就像等號=在C++和Java里的作用), 下面是我們的例子:

set(test, 5)            // 符號test的值為5
set(=, 5)               // 符號=的值為5
set(test, "hello")      // 符號test的值為字符串"hello"
set(test, =)            // 此時(shí)符號=的值為5, 所以test的也為5
set(*, "hello")         // 符號*的值為"hello"

好像有什么不對的地方? 假定我們對賦給整數(shù)或者字符串值, 那做乘法時(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)          // 錯(cuò)誤的表達(dá)式, *不再是乘法, 而是數(shù)值3
temp(3,4)       // temp是乘法函數(shù), 所以此表達(dá)式的值為3乘4等于12
set(*, temp)    // 再次把乘法函數(shù)賦予*
*(3,4)          // 3乘4等于12

再古怪一點(diǎn), 把減號的值賦給加號:

set(+, -)       // 減號(-)是內(nèi)置的減法函數(shù)
+(5, 4)         // 加號(+)現(xiàn)在是代表減法函數(shù), 結(jié)果是5減4等于1

這只是舉例子, 我還沒有詳細(xì)講函數(shù)水慨。Lisp中的函數(shù)是一種數(shù)據(jù)類型, 和整數(shù), 字符串,
符號等等一樣湿颅。一個(gè)函數(shù)并不必然有一個(gè)名字, 這和C++或者Java語言的情形很不相同蔚叨。
在這里函數(shù)自己代表自己床蜘。事實(shí)上它是一個(gè)指向代碼塊的指針, 附帶有一些其他信息(例
如一組參數(shù)變量)辙培。只有在把函數(shù)賦予其他符號時(shí), 它才具有了名字, 就像把一個(gè)數(shù)值或
字符串賦予變量一樣的道理。你可以用一個(gè)內(nèi)置的專門用于創(chuàng)建函數(shù)的函數(shù)來創(chuàng)建函數(shù),
然后把它賦值給符號fn, 用偽碼來表示就是:

fn [a]
{
    return *(a, 2);
}

這段代碼返回一個(gè)具有一個(gè)參數(shù)的函數(shù), 函數(shù)的功能是計(jì)算參數(shù)乘2的結(jié)果邢锯。這個(gè)函數(shù)還
沒有名字, 你可以把此函數(shù)賦值給別的符號:

set(times-two, fn [a] {return *(a, 2)})

我們現(xiàn)在可以這樣調(diào)用這個(gè)函數(shù):

time-two(5)         // 返回10

我們先跳過符號和函數(shù), 講一講表扬蕊。什么是表? 你也許已經(jīng)聽過好多相關(guān)的說法。表, 一
言以蔽之, 就是把類似XML那樣的數(shù)據(jù)塊, 用s表達(dá)式來表示丹擎。表用一對括號括住, 表中元
素以空格分隔, 表可以嵌套尾抑。例如(這回我們用真正的Lisp語法, 注意用分號表示注釋):

()                      ; 空表
(1)                     ; 含一個(gè)元素的表
(1 "test")              ; 兩元素表, 一個(gè)元素是整數(shù)1, 另一個(gè)是字符串
(test "hello")          ; 兩元素表, 一個(gè)元素是符號, 另一個(gè)是字符串
(test (1 2) "hello")    ; 三元素表, 一個(gè)符號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ù)也是表, 那就按照同樣的原則對這個(gè)表求值, 結(jié)果再傳遞給最初的函數(shù)作為
參數(shù)。這就是基本原則毁渗。我們看一下真正的代碼:

(* 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ù)賦予符號+
(+ 5 4)                 ; 依據(jù)上一句的結(jié)果, 此時(shí)+表示減法, 所以返回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中, 我們給表加一個(gè)前綴'來表示數(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來返回表的第一個(gè)元素, tail函數(shù)來返回剩余元素組成的
表府适。

(head '(* 3 4))         ; 返回符號*
(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ù)集十分精簡, 只包含了十分必要的部分檐春。剩下的函數(shù)都是作
為標(biāo)準(zhǔn)庫來實(shí)現(xiàn)的。

Lisp宏

我們已經(jīng)看到, 元編程在一個(gè)類似jsp的模板引擎方面的應(yīng)用么伯。我們通過簡單的字符串處
理來生成代碼疟暖。但是我們可以做的更好。我們先提一個(gè)問題, 怎樣寫一個(gè)工具, 通過查找
目錄結(jié)構(gòu)中的源文件來自動生成Ant腳本田柔。

用字符串處理的方式生成Ant腳本是一種簡單的方式俐巴。當(dāng)然, 還有一種更加抽象, 表達(dá)能
力更強(qiáng), 擴(kuò)展性更好的方式, 就是利用XML庫在內(nèi)存中直接生成XML節(jié)點(diǎn), 這樣的話內(nèi)存中
的節(jié)點(diǎn)就可以自動序列化成為字符串。不僅如此, 我們的工具還可以分析這些節(jié)點(diǎn), 對已
有的XML文件做變換硬爆。通過直接處理XML節(jié)點(diǎn)欣舵。我們可以超越字符串處理, 使用更高層次的
概念, 因此我們的工作就會做的更快更好。

我們當(dāng)然可以直接用Ant自身來處理XML變換和制作代碼生成工具缀磕≡等Γ或者我們也可以用Lisp
來做這項(xiàng)工作。正像我們以前所知的, 表是Lisp內(nèi)置的數(shù)據(jù)結(jié)構(gòu), Lisp含有大量的工具來
快速有效的操作表(head和tail是最簡單的兩個(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)程序啟動時(shí), 從
文件讀取這些數(shù)據(jù)并顯示給用戶遣疯。在別的語言里(比如說Java), 這個(gè)任務(wù)該怎么做? 我們
會解析XML文件, 從中得出任務(wù)表數(shù)據(jù), 然后寫代碼遍歷XML樹, 再轉(zhuǎn)換為Java的數(shù)據(jù)結(jié)構(gòu)
(老實(shí)講, 在Java里解析XML真不是件輕松的事情), 最后再把數(shù)據(jù)展示給用戶。現(xiàn)在如果
用Lisp, 該怎么做?

假定要用同樣思路的化, 我們大概會用Lisp庫來解析XML颠黎。XML對我們來說就是一個(gè)Lisp
的表(s表達(dá)式), 我們可以遍歷這個(gè)表, 然后把相關(guān)數(shù)據(jù)提交給用戶另锋。可是, 既然我們用
Lisp, 就根本沒有必要再用XML格式保存數(shù)據(jù), 直接用s表達(dá)式就好了, 這樣就沒有必要做
轉(zhuǎn)換了狭归。我們也用不著專門的解析庫, Lisp可以直接在內(nèi)存里處理s表達(dá)式夭坪。注意, Lisp
編譯器和.net編譯器一樣, 對Lisp程序來說, 在運(yùn)行時(shí)總是隨時(shí)可用的。

但是還有更好的辦法过椎。我們甚至不用寫表達(dá)式來存儲數(shù)據(jù), 我們可以寫宏, 把數(shù)據(jù)當(dāng)作代
碼來處理室梅。那該怎么做呢? 真的簡單【斡睿回想一下, Lisp的函數(shù)調(diào)用格式:

(function-name arg1 arg2 arg3)

其中每個(gè)參數(shù)都是s表達(dá)式, 求值以后, 傳遞給函數(shù)亡鼠。如果我們用(+ 4 5)來代替arg1,
那么, 程序會先求出結(jié)果, 就是9, 然后把9傳遞給函數(shù)。宏的工作方式和函數(shù)類似敷待。主要
的差別是, 宏的參數(shù)在代入時(shí)不求值间涵。

(macro-name (+ 4 5))

這里, (+ 4 5)作為一個(gè)表傳遞給宏, 然后宏就可以任意處理這個(gè)表, 當(dāng)然也可以對它求
值。宏的返回值是一個(gè)表, 然后有程序作為代碼來執(zhí)行榜揖。宏所占的位置, 就被替換為這個(gè)
結(jié)果代碼勾哩。我們可以定義一個(gè)宏把數(shù)據(jù)替換為任意代碼, 比方說, 替換為顯示數(shù)據(jù)給用戶
的代碼。

這和元編程, 以及我們要做的任務(wù)表程序有什么關(guān)系呢? 實(shí)際上, 編譯器會替我們工作,
調(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中, 此處的單引號應(yīng)當(dāng)是反單引號, 意思是對表不求值, 但可以對
表中某元素求值, 記號~表示對元素x求值, 這個(gè)求值記號在Common Lisp中應(yīng)當(dāng)是逗號。
反單引號和單引號的區(qū)別是, 單引號標(biāo)識的表, 其中的元素都不求值妨猩。這里作者所用的記
號是自己發(fā)明的一種Lisp方言Blaise, 和common lisp略有不同, 事實(shí)上, 發(fā)明方言是
lisp高手獨(dú)有的樂趣, 很多狂熱分子都熱衷這樣做潜叛。比如Paul Graham就發(fā)明了ARC, 許多
記號比傳統(tǒng)的Lisp簡潔得多, 顯得比較現(xiàn)代)

單引號的用處是禁止對表求值。每次程序中出現(xiàn)triple的時(shí)候,

(triple 4)

都會被替換成:

(+ 4 4 4)

我們可以為任務(wù)表程序?qū)懸粋€(gè)宏, 把任務(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)造了一個(gè)非常小的有限的語言來管理嵌在Lisp中的任務(wù)表威兜。這個(gè)語言只用來解決特
定領(lǐng)域的問題, 通常稱之為DSLs(特定領(lǐng)域語言, 或?qū)S妙I(lǐng)域語言)。

特定領(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)勢, 又有貿(mào)易語言的好處措伐。每天我們都會收獲這種方法帶給我們的益處, 遠(yuǎn)遠(yuǎn)
超過Ant所能給予我們的。

用DSL解決問題, 做出的程序精簡, 易于維護(hù), 富有彈性军俊。在Java里面, 我們可以用類來
處理問題侥加。這兩種方法的差別在于, Lisp使我們達(dá)到了一個(gè)更高層次的抽象, 我們不再受
語言解析器本身的限制, 比較一下用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的資源正在迅速增
加, 隨著時(shí)間推移, Lisp將會越來越受關(guān)注提前。

Lisp使人超越平庸, 走到前沿吗货。學(xué)會Lisp意味著你能找到更好的工作, 因?yàn)槁斆鞯墓椭鲿?br> 被你與眾不同的洞察力所打動。學(xué)會Lisp也可能意味著明天你可能會被解雇, 因?yàn)槟憧偸?br> 強(qiáng)調(diào), 如果公司所有軟件都用Lisp寫, 公司將會如何卓越, 而這些話你的同事會聽煩的狈网。
Lisp值得努力學(xué)習(xí)嗎? 那些已經(jīng)學(xué)會Lisp的人都說值得, 當(dāng)然, 這取決于你的判斷宙搬。

你的看法呢?

這篇文章寫寫停停, 用了幾個(gè)月才最終完成。如果你覺得有趣, 或者有什么問題, 意見或
建議, 請給我發(fā)郵件coffeemug@gmail.com, 我會很高興收到你的反饋拓哺。

                         作者 Slava Akhmechet
                         譯者 Alec Jang

          出處: http://www.defmacro.org/ramblings/lisp.html

The Nature of Lisp
Monday, May 8, 2006
Introduction
When I first stumbled into Lisp advocacy on various corners of the web I was already an experienced programmer. At that point I had grokked what seemed at the time a wide range of programming languages. I was proud to have the usual suspects (C++, Java, C#, etc.) on my service record and was under impression that I knew everything there is to know about programming languages. I couldn't have possibly been more wrong.

My initial attempt to learn Lisp came to a crashing halt as soon as I saw some sample code. I suppose the same thought ran through my mind that ran through thousands of other minds who were ever in my shoes: "Why on Earth would anyone want to use a language with such horrific syntax?!" I couldn't be bothered to learn a language if its creators couldn't be bothered to give it a pleasant syntax. After all, I was almost blinded by the infamous Lisp parentheses!

The moment I regained my sight I communicated my frustrations to some members of the Lisp sect. Almost immediately I was bombarded by a standard set of responses: Lisp's parentheses are only a superficial matter, Lisp has a huge benefit of code and data being expressed in the same manner (which, obviously, is a huge improvement over XML), Lisp has tremendously powerful metaprogramming facilities that allow programs to write code and modify themselves, Lisp allows for creation of mini-languages specific to the problem at hand, Lisp blurs the distinction between run time and compile time, Lisp, Lisp, Lisp... The list was very impressive. Needless to say none of it made sense. Nobody could illustrate the usefulness of these features with specific examples because these techniques are supposedly only useful in large software systems. After many hours of debating that conventional programming languages do the job just fine, I gave up. I wasn't about to invest months into learning a language with a terrible syntax in order to understand obscure features that had no useful examples. My time has not yet come.

For many months the Lisp advocates pressed on. I was baffled. Many extremely intelligent people I knew and had much respect for were praising Lisp with almost religious dedication. There had to be something there, something I couldn't afford not to get my hands on! Eventually my thirst for knowledge won me over. I took the plunge, bit the bullet, got my hands dirty, and began months of mind bending exercises. It was a journey on an endless lake of frustration. I turned my mind inside out, rinsed it, and put it back in place. I went through seven rings of hell and came back. And then I got it.

The enlightenment came instantaneously. One moment I understood nothing, and the next moment everything clicked into place. I've achieved nirvana. Dozens of times I heard Eric Raymond's statement quoted by different people: "Lisp is worth learning for the profound enlightenment experience you will have when you finally get it; that experience will make you a better programmer for the rest of your days, even if you never actually use Lisp itself a lot." I never understood this statement. I never believed it could be true. And finally, after all the pain, it made sense! There was more truth to it than I ever could have imagined. I've achieved an almost divine state of mind, an instantaneous enlightenment experience that turned my view of computer science on its head in less than a single second.

That very second I became a member of the Lisp cult. I felt something a ninjitsu master must feel: I had to spread my newfound knowledge to at least ten lost souls in the course of my lifetime. I took the usual path. I was rehashing the same arguments that were given to me for years (only now they actually made sense!), hoping to convert unsuspecting bystanders. It didn't work. My persistence sparked a few people's interest but their curiosity dwindled at the mere sight of sample Lisp code. Perhaps years of advocacy would forge a few new Lispers, but I wasn't satisfied. There had to be a better way.

I gave the matter careful thought. Is there something inherently hard about Lisp that prevents very intelligent, experienced programmers from understanding it? No, there isn't. After all, I got it, and if I can do it, anybody can. Then what is it that makes Lisp so hard to understand? The answer, as such things usually do, came unexpectedly. Of course! Teaching anybody anything involves building advanced concepts on top of concepts they already understand! If the process is made interesting and the matter is explained properly the new concepts become as intuitive as the original building blocks that aided their understanding. That was the problem! Metaprogramming, code and data in one representation, self-modifying programs, domain specific mini-languages, none of the explanations for these concepts referenced familiar territory. How could I expect anyone to understand them! No wonder people wanted specific examples. I could as well have been speaking in Martian!

I shared my ideas with fellow Lispers. "Well, of course these concepts aren't explained in terms of familiar territory", they said. "They are so different, they're unlike anything these people have learned before." This was a poor excuse. "I do not believe this to be true", I said. The response was unanimous: "Why don't you give it a try?" So I did. This article is a product of my efforts. It is my attempt to explain Lisp in familiar, intuitive concepts. I urge brave souls to read on. Grab your favorite drink. Take a deep breath. Prepare to be blown away. Oh, and may the Force be with you.

XML Reloaded
A thousand mile journey starts with a single step. A journey to enlightenment is no exception and our first step just happens to be XML. What more could possibly be said about XML that hasn't already been said? It turns out, quite a bit. While there's nothing particularly interesting about XML itself, its relationship to Lisp is fascinating. XML is the all too familiar concept that Lisp advocates need so much. It is our bridge to conveying understanding to regular programmers. So let's revive the dead horse, take out the stick, and venture into XML wilderness that no one dared venture into before us. It's time to see the all too familiar moon from the other side.

Superficially XML is nothing more than a standardized syntax used to express arbitrary hierarchical data in human readable form. To-do lists, web pages, medical records, auto insurance claims, configuration files are all examples of potential XML use. Let's use a simple to-do list as an example (in a couple of sections you'll see it in a whole new light):

<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>
What happens if we unleash our favorite XML parser on this to-do list? Once the data is parsed, how is it represented in memory? The most natural representation is, of course, a tree - a perfect data structure for hierarchical data. After all is said and done, XML is really just a tree serialized to a human readable form. Anything that can be represented in a tree can be represented in XML and vice versa. I hope you understand this idea. It's very important for what's coming next.

Let's take this a little further. What other type of data is often represented as a tree? At this point the list is as good as infinite so I'll give you a hint at what I'm getting at - try to remember your old compiler course. If you have a vague recollection that source code is stored in a tree after it's parsed, you're on the right track. Any compiler inevitably parses the source code into an abstract syntax tree. This isn't surprising since source code is hierarchical: functions contain arguments and blocks of code. Blocks of code contain expressions and statements. Expressions contain variables and operators. And so it goes.

Let's apply our corollary that any tree can easily be serialized into XML to this idea. If all source code is eventually represented as a tree, and any tree can be serialized into XML, then all source code can be converted to XML, right? Let's illustrate this interesting property by a simple example. Consider the function below:

int add(int arg1, int arg2)
{
return arg1 + arg2;
}
Can you convert this function definition to its XML equivalent? Turns out, it's reasonably simple. Naturally there are many ways to do this. Here is one way the resulting XML can look like:

<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>
We can go through this relatively simple exercise with any language. We can turn any source code into XML, and we can transform the resulting XML back to original source code. We can write a converter that turns Java into XML and a converter that turns XML back to Java. We could do the same for C++. (In case you're wondering if anyone is crazy enough to do it, take a look at GCC-XML). Furthermore, for languages that share common features but use different syntax (which to some extent is true about most mainstream languages) we could convert source code from one language to another using XML as an intermediary representation. We could use our Java2XML converter to convert a Java program to XML. We could then run an XML2CPP converter on the resulting XML and turn it into C++ code. With any luck (if we avoid using features of Java that don't exist in C++) we'll get a working C++ program. Neat, eh?

All this effectively means that we can use XML for generic storage of source code. We'd be able to create a whole class of programming languages that use uniform syntax, as well as write transformers that convert existing source code to XML. If we were to actually adopt this idea, compilers for different languages wouldn't need to implement parsers for their specific grammars - they'd simply use an XML parser to turn XML directly into an abstract syntax tree.

By now you're probably wondering why I've embarked on the XML crusade and what it has to do with Lisp (after all, Lisp was created about thirty years before XML). I promise that everything will become clear soon enough. But before we take our second step, let's go through a small philosophical exercise. Take a good look at the XML version of our "add" function above. How would you classify it? Is it data or code? If you think about it for a moment you'll realize that there are good reasons to put this XML snippet into both categories. It's XML and it's just information encoded in a standardized format. We've already determined that it can be generated from a tree data structure in memory (that's effectively what GCC-XML does). It's lying around in a file with no apparent way to execute it. We can parse it into a tree of XML nodes and do various transformations on it. It's data. But wait a moment! When all is said and done it's the same "add" function written with a different syntax, right? Once parsed, its tree could be fed into a compiler and we could execute it. We could easily write a small interpreter for this XML code and we could execute it directly. Alternatively, we could transform it into Java or C++ code, compile it, and run it. It's code.

So, where are we? Looks like we've just arrived to an interesting point. A concept that has traditionally been so hard to understand is now amazingly simple and intuitive. Code is also always data! Does it mean that data is also always code? As crazy as this sounds this very well might be the case. Remember how I promised that you'll see our to-do list in a whole new light? Let me reiterate on that promise. But we aren't ready to discuss this just yet. For now let's continue walking down our path.

A little earlier I mentioned that we could easily write an interpreter to execute our XML snippet of the add function. Of course this sounds like a purely theoretical exercise. Who in their right mind would want to do that for practical purposes? Well, it turns out quite a few people would disagree. You've likely encountered and used their work at least once in your career, too. Do I have you out on the edge of your seat? If so, let's move on!

Ant Reloaded
Now that we've made the trip to the dark side of the moon, let's not leave quite yet. We may still learn something by exploring it a little more, so let's take another step. We begin by closing our eyes and remembering a cold rainy night in the winter of 2000. A prominent developer by the name of James Duncan Davidson1 was hacking his way through Tomcat servlet container. As the time came to build the changes he carefully saved all his files and ran make. Errors. Lots of errors. Something was wrong. After careful examination James exclaimed: "Is my command not executing because I have a space in front of my tab?!" Indeed, this was the problem. Again. James has had enough. He could sense the full moon through the clouds and it made him adventurous. He created a fresh Java project and quickly hacked together a simple but surprisingly useful utility. This spark of genius used Java property files for information on how to build the project. James could now write the equivalent of the makefile in a nice format without worrying about the damned spaces ever again. His utility did all the hard work by interpreting the property file and taking appropriate actions to build the project. It was neat. Another Neat Tool. Ant.

After using Ant to build Tomcat for a few months it became clear that Java property files are not sufficient to express complicated build instructions. Files needed to be checked out, copied, compiled, sent to another machine, and unit tested. In case of failure e-mails needed to be sent out to appropriate people. In case of success "Bad to the Bone" needed to be played at the highest possible volume. At the end of the track volume had to be restored to its original level. Yes, Java property files didn't cut it anymore. James needed a more flexible solution. He didn't feel like writing his own parser (especially since he wanted an industry standard solution). XML seemed like a reasonable alternative. In a couple of days Ant was ported to XML. It was the best thing since sliced bread.

So how does Ant work? It's pretty simple. It takes an XML file with specific build instructions (you decide if they're data or code) and interprets them by running specialized Java code for each XML element. It's actually much simpler than it sounds. A simple XML instruction like the one below causes a Java class with an equivalent name to be loaded and its code to be executed.

<copy todir="../new/dir">
<fileset dir="src_dir"/>
</copy>
The snippet above copies a source directory to a destination directory. Ant locates a "copy" task (a Java class, really), sets appropriate parameters (todir and fileset) by calling appropriate Java methods and then executes the task. Ant comes with a set of core tasks and anyone can extend it with tasks of their own simply by writing Java classes that follow certain conventions. Ant finds these classes and executes them whenever XML elements with appropriate names are encountered. Pretty simple. Effectively Ant accomplishes what we were talking about in the previous section: it acts as an interpreter for a language that uses XML as its syntax by translating XML elements to appropriate Java instructions. We could write an "add" task and have Ant execute it when it encounters the XML snippet for addition presented in the previous section! Considering that Ant is an extremely popular project, the ideas presented in the previous section start looking more sane. After all, they're being used every day in what probably amounts to thousands of companies!

So far I've said nothing about why Ant actually goes through all the trouble of interpreting XML. Don't try to look for the answer on its website either - you'll find nothing of value. Nothing relevant to our discussion, anyway. Let's take another step. It's time to find out why.

Why XML?
Sometimes right decisions are made without full conscious understanding of all the issues involved. I'm not sure if James knew why he chose XML - it was likely a subconscious decision. At the very least, the reasons I saw on Ant's website for using XML are all the wrong reasons. It appears that the main concerns revolved around portability and extensibility. I fail to see how XML helps advance these goals in Ant's case. What is the advantage of using interpreted XML over simple Java source code? Why not create a set of classes with a nice API for commonly used tasks (copying directories, compiling, etc.) and using those directly from Java source code? This would run on every platform that runs Java (which Ant requires anyway), it's infinitely extensible, and it has the benefit of having a more pleasant, familiar syntax. So why XML? Can we find a good reason for using it?

It turns out that we can (although as I mentioned earlier I'm not sure if James was consciously aware of it). XML has the property of being far more flexible in terms of introduction of semantic constructs than Java could ever hope to be. Don't worry, I'm not falling into the trap of using big words to describe incomprehensible concepts. This is actually a relatively simple idea, though it may take some effort to explain. Buckle your seat-belt. We're about to make a giant leap towards achieving nirvana.

How can we represent 'copy' example above in Java code? Here's one way to do it:

CopyTask copy = new CopyTask();
Fileset fileset = new Fileset();

fileset.setDir("src_dir");
copy.setToDir("../new/dir");
copy.setFileset(fileset);

copy.execute();
The code is almost the same, albeit a little longer than the original XML. So what's different? The answer is that the XML snippet introduces a special semantic construct for copying. If we could do it in Java it would look like this:

copy("../new/dir")
{
fileset("src_dir");
}
Can you see the difference? The code above (if it were possible in Java) is a special operator for copying files - similar to a for loop or a new foreach construct introduced in Java 5. If we had an automatic converter from XML to Java it would likely produce the above gibberish. The reason for this is that Java's accepted syntax tree grammar is fixed by the language specification - we have no way of modifying it. We can add packages, classes, methods, but we cannot extend Java to make addition of new operators possible. Yet we can do it to our heart's content in XML - its syntax tree isn't restricted by anything except our interpreter! If the idea is still unclear, consider introducing a special operator 'unless' to Java:

unless(someObject.canFly())
{
someObject.transportByGround();
}
In the previous two examples we extend the Java language to introduce an operator for copying files and a conditional operator unless. We would do this by modifying the abstract syntax tree grammar that Java compiler accepts. Naturally we cannot do it with standard Java facilities, but we can easily do it in XML. Because our XML interpreter parses the abstract syntax tree that results from it, we can extend it to include any operator we like.

For complex operators this ability provides tremendous benefits. Can you imagine writing special operators for checking out source code, compiling files, running unit testing, sending email? Try to come up with some. If you're dealing with a specialized problem (in our case it's building projects) these operators can do wonders to decrease the amount of code you have to type and to increase clarity and code reuse. Interpreted XML makes this extremely easy to accomplish because it's a simple data file that stores hierarchical data. We do not have this option in Java because it's hierarchical structure is fixed (as you will soon find out, we do have this option in Lisp). Perhaps this is one of the reasons why Ant is so successful?

I urge you to take a look at recent evolution of Java and C# (especially the recently released specification for C# 3.0). The languages are being evolved by abstracting away commonly used functionality and adding it in the form of operators. New C# operators for built-in queries is one example. This is accomplished by relatively traditional means: language creators modify the accepted abstract syntax tree and add implementations of certain features. Imagine the possibilities if the programmer could modify the abstract syntax tree himself! Whole new sub-languages could be built for specialized domains (for example a language for building projects, like Ant). Can you come up with other examples? Think about these concepts for a bit, but don't worry about them too much. We'll come back to these issues after introducing a few more ideas. By then things will be a little more clear.

Almost Lisp
Let's forget about the operator business for the moment and try to expand our horizons beyond the constraints of Ant's design. I mentioned earlier that Ant can be extended by writing conventional Java classes. Ant interpreter then attempts to match XML elements to appropriately named Java classes and if the match is found the task is executed. An interesting question begs to be asked. Why not extend Ant in Ant itself? After all, core tasks contain a lot of conventional programming language constructs ('if' being a perfect example). If Ant provided constructs to develop tasks in Ant itself we'd reach a higher degree of portability. We'd be dependent on a core set of tasks (a standard library, if you will) and we wouldn't care if Java runtime is present: the core set could be implemented in anything. The rest of the tasks would be built on top of the core using Ant-XML itself. Ant would then become a generic, extensible, XML-based programming language. Consider the possibilities:

<task name="Test">
<echo message="Hello World!"/>
</task>
<Test />
If ant supported the "task" construct, the example above would print "Hello World!". In fact, we could write a "task" task in Java and make Ant able to extend itself using Ant-XML! Ant would then be able to build more complicated primitives on top of simple ones, just like any other programming language! This is an example of "XML" based programming language we were talking about in the beginning of this tutorial. Not very useful (can you tell why?) but pretty damn cool.

By the way, take a look at our 'Test' task once again. Congratulations. You're looking at Lisp code. What on Earth am I talking about? It doesn't look anything like Lisp? Don't worry, we'll fix that in a bit. Confused? Good. Let's clear it all up!

A Better XML
I mentioned in the previous section that self-extending Ant wouldn't be very useful. The reason for that is XML's verbosity. It's not too bad for data files but the moment you try writing reasonably complex code the amount of typing you have to do quickly starts to get in the way and progresses to becoming unusable for any real project. Have you ever tried writing Ant build scripts? I have, and once they get complex enough having to do it in XML becomes really annoying. Imagine having to type almost everything in Java twice because you have to close every element. Wouldn't that drive you nuts?

The solution to this problem involves using a less verbose alternative to XML. Remember, XML is just a format for representing hierarchical data. We don't have to use XML's angle brackets to serialize trees. We could come up with many other formats. One such format (incidentally, the one Lisp uses) is called an s-expression. S-expressions accomplish the same goals as XML. They're just a lot less verbose, which makes them much better suited for typing code. I will explain s-expressions in a little while, but before I do I have to clear up a few things about XML. Let's consider our XML example for copying files:

<copy todir="../new/dir">
<fileset dir="src_dir"/>
</copy>
Think of what the parse tree of this snippet would look like in memory. We'd have a 'copy' node that contains a fileset node. But what about attributes? How do they fit into our picture? If you've ever used XML to describe data and wondered whether you should use an element or an attribute, you're not alone. Nobody can really figure this out and doing it right tends to be black magic rather than science. The reason for that is that attributes are really subsets of elements. Anything attributes can do, elements can do as well. The reason attributes were introduced is to curb XML's verbosity. Take a look at another version of our 'copy' snippet:

<copy>
<todir>../new/dir</todir>
<fileset>
<dir>src_dir</dir>
</fileset>
</copy>
The two snippets hold exactly the same information. However, we use attributes to avoid typing the same thing more than once. Imagine if attributes weren't part of XML specification. Writing anything in XML would drive us nuts!

Now that we got attributes out of the way, let's look at s-expressions. The reason we took this detour is that s-expressions do not have attributes. Because they're a lot less verbose, attributes are simply unnecessary. This is one thing we need to keep in mind when transforming XML to s-expressions. Let's take a look at an example. We could translate above snippet to s-expressions like this:

(copy
(todir "../new/dir")
(fileset (dir "src_dir")))
Take a good look at this representation. What's different? Angle brackets seem to be replaced by parentheses. Instead of enclosing each element into a pair of parentheses and then closing each element with a "(/element)" we simply skip the second parenthesis in "(element" and proceed. The element is then closed like this: ")". That's it! The translation is natural and very simple. It's also a lot easier to type. Do parentheses blind first time users? Maybe, but now that we're understand the reasoning behind them they're a lot easier to handle. At the very least they're better than arthritis inducing verbosity of XML. After you get used to s-expressions writing code in them is not only doable but very pleasant. And they provide all the benefits of writing code in XML (many of which we're yet to explore). Let's take a look at our 'task' code in something that looks a lot more like lisp:

(task (name "Test")
(echo (message "Hello World!")))

(Test)
S-expressions are called lists in Lisp lingo. Consider our 'task' element above. If we rewrite it without a line break and with comas instead of spaces it's starting to look surprisingly like a list of elements and other lists (the formatting is added to make it easier to see nested lists):

(task, (name, "test"), (echo, (message, "Hello World!")))
We could do the same with XML. Of course the line above isn't really a list, it's a tree, just like its XML-alternative. Don't let references to lists confuse you, it's just that lists that contain other lists and trees are effectively the same thing. Lisp may stand for List Processing, but it's really tree processing - no different than processing XML nodes.

Whew. After much rambling we finally got to something that looks like Lisp (and is Lisp, really). By now the mysterious Lisp parentheses as well as some claims made by Lisp advocates should become more clear. But we still have a lot of ground to cover. Ready? Let's move on!

C Macros Reloaded
By now you must be tired of all the XML talk. I'm tired of it as well. It's time to take a break from all the trees, s-expressions, and Ant business. Instead, let's go back to every programmer's roots. It's time to talk about C preprocessor. What's C got to do with anything, I hear you ask? Well, we now know enough to get into metaprogramming and discuss code that writes other code. Understanding this tends to be hard since all tutorials discuss it in terms of languages that you don't know. But there is nothing hard about the concept. I believe that a metaprogramming discussion based on C will make the whole thing much easier to understand. So, let's see (pun intended).

Why would anyone want to write a program that writes programs? How can we use something like this in the real world? What on Earth is metaprogramming, anyway? You already know all the answers, you just don't know it yet. In order to unlock the hidden vault of divine knowledge let's consider a rather mundane task of simple database access from code. We've all been there. Writing SQL queries all over the code to modify data within tables turns into repetitive hell soon enough. Even with the new C# 3.0 LINQ stuff this is a huge pain. Writing a full SQL query (albeit with a nice built in syntax) to get someone's name or to modify someone's address isn't exactly a programmer's idea of comfort. What do we do to solve these problems? Enter data access layers.

The idea is simple enough. You abstract database access (at least trivial queries, anyway) by creating a set of classes that mirror the tables in the database and use accessor methods to execute actual queries. This simplifies development tremendously - instead of writing SQL queries we make simple method calls (or property assignments, depending on your language of choice). Anyone who has ever used even the simplest of data access layers knows how much time it can save. Of course anyone who has ever written one knows how much time it can kill - writing a set of classes that mirror tables and convert accessors to SQL queries takes a considerable chunk of time. This seems especially silly since most of the work is manual: once you figure out the design and develop a template for your typical data access class you don't need to do any thinking. You just write code based on the same template over and over and over and over again. Many people figured out that there is a better way - there are plenty of tools that connect to the database, grab the schema, and write code for you based on a predefined (or a custom) template.

Anyone who has ever used such a tool knows what an amazing time saver it can be. In a few clicks you connect the tool to the database, get it to generate the data access layer source code, add the files to your project and voilà - ten minutes worth of work do a better job than hundreds of man-hours that were required previously. What happens if your database schema changes? Well, you just have to go through this short process again. Of course some of the best tools let you automate this - you simply add them as a part of your build step and every time you compile your project everything is done for you automatically. This is perfect! You barely have to do anything at all. If the schema ever changes your data access layer code updates automatically at compile time and any obsolete access in your code will result in compiler errors!

Data access layers are one good example, but there are plenty of others. From boilerplate GUI code, to web code, to COM and CORBA stubs, to MFC and ATL, - there are plenty of examples where the same code is written over and over again. Since writing this code is a task that can be automated completely and a programmer's time is far more expensive than CPU time, plenty of tools have been created that generate this boilerplate code automatically. What are these tools, exactly? Well, they are programs that write programs. They perform a simple task that has a mysterious name of metaprogramming. That's all there is to it.

We could create and use such tools in millions of scenarios but more often than not we don't. What it boils down to is a subconscious calculation - is it worth it for me to create a separate project, write a whole tool to generate something, and then use it, if I only have to write these very similar pieces about seven times? Of course not. Data access layers and COM stubs are written hundreds, thousands of times. This is why there are tools for them. For similar pieces of code that repeat only a few times, or even a few dozen times, writing code generation tools isn't even considered. The trouble to create such a tool more often than not far outweighs the benefit of using one. If only creating such tools was much easier, we could use them more often, and perhaps save many hours of our time. Let's see if we can accomplish this in a reasonable manner.

Surprisingly C preprocessor comes to the rescue. We've all used it in C and C++. On occasion we all wish Java had it. We use it to execute simple instructions at compile time to make small changes to our code (like selectively removing debug statements). Let's look at a quick example:

define triple(X) X + X + X

What does this line do? It's a simple instruction written in the preprocessor language that instructs it to replace all instances of triple(X) with X + X + X. For example all instances of 'triple(5)' will be replaced with '5 + 5 + 5' and the resulting code will be compiled by the C compiler. We're really doing a very primitive version of code generation here. If only C preprocessor was a little more powerful and included ways to connect to the database and a few more simple constructs, we could use it to develop our data access layer right there, from within our program! Consider the following example that uses an imaginary extension of the C preprocessor:

get-db-schema("127.0.0.1, un, pwd");

iterate-through-tables

for-each-table

class #table-name
{
};

end-for-each

We've just connected to the database schema, iterated through all the tables, and created an empty class for each. All in a couple of lines right within our source code! Now every time we recompile the file where above code appears we'll get a freshly built set of classes that automatically update based on the schema. With a little imagination you can see how we could build a full data access layer straight from within our program, without the use of any external tools! Of course this has a certain disadvantage (aside from the fact that such an advanced version of C preprocessor doesn't exist) - we'd have to learn a whole new "compile-time language" to do this sort of work. For complex code generation this language would have to be very complex as well, it would have to support many libraries and language constructs. For example, if our generated code depended on some file located at some ftp server the preprocessor would have to be able to connect to ftp. It's a shame to create and learn a new language just to do this. Especially since there are so many nice languages already out there. Of course if we add a little creativity we can easily avoid this pitfall.

Why not replace the preprocessor language with C/C++ itself? We'd have full power of the language at compile time and we'd only need to learn a few simple directives to differentiate between compile time and runtime code!

<%
cout << "Enter a number: ";
cin >> n;
%>
for(int i = 0; i < <%= n %>; i++)
{
cout << "hello" << endl;
}
Can you see what happens here? Everything that's between <% and %> tags runs when the program is compiled. Anything outside of these tags is normal code. In the example above you'd start compiling your program in the development environment. The code between the tags would be compiled and then ran. You'd get a prompt to enter a number. You'd enter one and it would be placed inside the for loop. The for loop would then be compiled as usual and you'd be able to execute it. For example, if you'd enter 5 during the compilation of your program, the resulting code would look like this:

for(int i = 0; i < 5; i++)
{
cout << "hello" << endl;
}
Simple and effective. No need for a special preprocessor language. We get full power of our host language (in this case C/C++) at compile time. We could easily connect to a database and generate our data access layer source code at compile time in the same way JSP or ASP generate HTML! Creating such tools would also be tremendously quick and simple. We'd never have to create new projects with specialized GUIs. We could inline our tools right into our programs. We wouldn't have to worry about whether writing such tools is worth it because writing them would be so fast - we could save tremendous amounts of time by creating simple bits of code that do mundane code generation for us!

Hello, Lisp!
Everything we've learned about Lisp so far can be summarized by a single statement: Lisp is executable XML with a friendlier syntax. We haven't said a single word about how Lisp actually operates. It's time to fill this gap2.

Lisp has a number of built in data types. Integers and strings, for example, aren't much different from what you're used to. The meaning of 71 or "hello" is roughly the same in Lisp as in C++ or Java. What is of more interest to us are symbols, lists, and functions. I will spend the rest of this section describing these data types as well as how a Lisp environment compiles and executes the source code you type into it (this is called evaluation in Lisp lingo). Getting through this section in one piece is important for understanding true potential of Lisp's metaprogramming, the unity of code and data, and the notion of domain specific languages. Don't think of this section as a chore though, I'll try to make it fun and accessible. Hopefully you can pick up a few interesting ideas on the way. Ok. Let's start with Lisp's symbols.

A symbol in Lisp is roughly equivalent to C++ or Java's notion of an identifier. It's a name you can use to access a variable (like currentTime, arrayCount, n, etc.) The difference is that a symbol in Lisp is a lot more liberal than its mainstream identifier alternative. In C++ or Java you're limited to alphanumeric characters and an underscore. In Lisp, you are not. For example + is a valid symbol. So is -, =, hello-world, hello+world, *, etc. (you can find the exact definition of valid Lisp symbols online). You can assign to these symbols any data-type you like. Let's ignore Lisp syntax and use pseudo-code for now. Assume that a function set assigns some value to a symbol (like = does in Java or C++). The following are all valid examples:

set(test, 5) // symbol 'test' will equal an integer 5
set(=, 5) // symbol '=' will equal an integer 5
set(test, "hello") // symbol 'test' will equal a string "hello"
set(test, =) // at this point symbol '=' is equal to 5
// therefore symbol 'test' will equal to 5
set(, "hello") // symbol '' will equal a string "hello"
At this point something must smell wrong. If we can assign strings and integers to symbols like *, how does Lisp do multiplication? After all, * means multiply, right? The answer is pretty simple. Functions in Lisp aren't special. There is a data-type, function, just like integer and string, that you assign to symbols. A multiplication function is built into Lisp and is assigned to a symbol *. You can reassign a different value to * and you'd lose the multiplication function. Or you can store the value of the function in some other variable. Again, using pseudo-code:

(3, 4) // multiplies 3 by 4, resulting in 12
set(temp, ) // symbol '' is equal to the multiply function
// so temp will equal to the multiply function
set(
, 3) // sets symbol '' to equal to 3
(3, 4) // error, symbol '' no longer equals to a function
// it's equal to 3
temp(3, 4) // temp equals to a multiply function
// so Lisp multiplies 3 by 4 resulting in 12
set(
, temp) // symbol '*' equals multiply function again
*(3, 4) // multiplies 3 by 4, resulting in 12
You can even do wacky stuff like reassigning plus to minus:

set(+, -) // the value of '-' is a built in minus function
// so now symbol '+' equals to a minus function
+(5, 4) // since symbol '+' is equal to the minus function
// this results in 1
I've used functions quite liberally in these examples but I didn't describe them yet. A function in Lisp is just a data-type like an integer, a string, or a symbol. A function doesn't have a notion of a name like in Java or C++. Instead, it stands on its own. Effectively it is a pointer to a block of code along with some information (like a number of parameters it accepts). You only give the function a name by assigning it to a symbol, just like you assign an integer or a string. You can create a function by using a built in function for creating functions, assigned to a symbol 'fn'. Using pseudo-code:

fn [a]
{
return *(a, 2);
}
This returns a function that takes a single parameter named 'a' and doubles it. Note that the function has no name but you can assign it to a symbol:

set(times-two, fn [a] { return *(a, 2); })
We can now call this function:

times-two(5) // returns 10
Now that we went over symbols and functions, what about lists? Well, you already know a lot about them. Lists are simply pieces of XML written in s-expression form. A list is specified by parentheses and contains Lisp data-types (including other lists) separated by a space. For example (this is real Lisp, note that we use semicolons for comments now):

() ; an empty list
(1) ; a list with a single element, 1
(1 "test") ; a list with two elements
; an integer 1 and a string "test"
(test "hello") ; a list with two elements
; a symbol test and a string "hello"
(test (1 2) "hello") ; a list with three elements, a symbol test
; a list of two integers 1 and 2
; and a string "hello"
When a Lisp system encounters lists in the source code it acts exactly like Ant does when it encounters XML - it attempts to execute them. In fact, Lisp source code is only specified using lists, just like Ant source code is only specified using XML. Lisp executes lists in the following manner. The first element of the list is treated as the name of a function. The rest of the elements are treated as functions parameters. If one of the parameters is another list it is executed using the same principles and the result is passed as a parameter to the original function. That's it. We can write real code now:

(* 3 4) ; equivalent to pseudo-code (3, 4).
; Symbol '
' is a function
; 3 and 4 are its parameters.
; Returns 12.
(times-two 5) ; returns 10
(3 4) ; error: 3 is not a function
(times-two) ; error, times-two expects one parameter
(times-two 3 4) ; error, times-two expects one parameter
(set + -) ; sets symbol '+' to be equal to whatever symbol '-'
; equals to, which is a minus function
(+ 5 4) ; returns 1 since symbol '+' is now equal
; to the minus function
(* 3 (* 2 2)) ; multiplies 3 by the second parameter
; (which is a function call that returns 4).
; Returns 12.
Note that so far every list we've specified was treated by a Lisp system as code. But how can we treat a list as data? Again, imagine an Ant task that accepts XML as one of its parameters. In Lisp we do this using a quote operator ' like so:

(set test '(1 2)) ; test is equal to a list of two integers, 1 and 2
(set test (1 2)) ; error, 1 is not a function
(set test '(* 3 4)) ; sets test to a list of three elements,
; a symbol *, an integer 3, and an integer 4
We can use a built in function head to return the first element of the list, and a built in function tail to return the rest of the list's elements:

(head '(* 3 4)) ; returns a symbol ''
(tail '(
3 4)) ; returns a list (3 4)
(head (tail '( * 3 4))) ; (tail '(* 3 4)) returns a list (3 4)
; and (head '(3 4)) returns 3.
(head test) ; test was set to a list in previous example
; returns a symbol '*'
You can think of built in Lisp functions as you think of Ant tasks. The difference is that we don't have to extend Lisp in another language (although we can), we can extend it in Lisp itself as we did with the times-two example. Lisp comes with a very compact set of built in functions - the necessary minimum. The rest of the language is implemented as a standard library in Lisp itself.

Lisp Macros
So far we've looked at metaprogramming in terms of a simple templating engine similar to JSP. We've done code generation using simple string manipulations. This is generally how most code generation tools go about doing this task. But we can do much better. To get on the right track, let's start off with a question. How would we write a tool that automatically generates Ant build scripts by looking at source files in the directory structure?

We could take the easy way out and generate Ant XML by manipulating strings. Of course a much more abstract, expressive and extensible way is to work with XML processing libraries to generate XML nodes directly in memory. The nodes can then be serialized to strings automatically. Furthermore, our tool would be able to analyze and transform existing Ant build scripts by loading them and dealing with the XML nodes directly. We would abstract ourselves from strings and deal with higher level concepts which let us get the job done faster and easier.

Of course we could write Ant tasks that allow dealing with XML transformations and write our generation tool in Ant itself. Or we could just use Lisp. As we saw earlier, a list is a built in Lisp data structure and Lisp has a number of facilities for processing lists quickly and effectively (head and tail being the simplest ones). Additionally Lisp has no semantic constraints - you can have your code (and data) have any structure you want.

Metaprogramming in Lisp is done using a construct called a "macro". Let's try to develop a set of macros that transform data like, say, a to-do list (surprised?), into a language for dealing with to-do lists.

Let's recall our to-do list example. The XML looks like this:

<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>
The corresponding s-expression version looks like this:

(todo "housework"
(item (priority high) "Clean the house.")
(item (priority medium) "Wash the dishes.")
(item (priority medium) "Buy more soap."))
Suppose we're writing a to-do manager application. We keep our to-do items serialized in a set of files and when the program starts up we want to read them and display them to the user. How would we do this with XML and some other language (say, Java)? We'd parse our XML files with the to-do lists using some XML parser, write the code that walks the XML tree and converts it to a Java data structure (because frankly, processing DOM in Java is a pain in the neck), and then use this data structure to display the data. Now, how would we do the same thing in Lisp?

If we were to adopt the same approach we'd parse the files using Lisp libraries responsible for parsing XML. The XML would then be presented to us as a Lisp list (an s-expression) and we'd walk the list and present relevant data to the user. Of course if we used Lisp it would make sense to persist the data as s-expressions directly as there's no reason to do an XML conversion. We wouldn't need special parsing libraries since data persisted as a set of s-expressions is valid Lisp and we could use Lisp compiler to parse it and store it in memory as a Lisp list. Note that Lisp compiler (much like .NET compiler) is available to a Lisp program at runtime.

But we can do better. Instead of writing code to walk the s-expression that stores our data we could write a macro that allows us to treat data as code! How do macros work? Pretty simple, really. Recall that a Lisp function is called like this:

(function-name arg1 arg2 arg3)
Where each argument is a valid Lisp expression that's evaluated and passed to the function. For example if we replace arg1 above with (+ 4 5), it will be evaluated and 9 would be passed to the function. A macro works the same way as a function, except its arguments are not evaluated.

(macro-name (+ 4 5))
In this case, (+ 4 5) is not evaluated and is passed to the macro as a list. The macro is then free to do what it likes with it, including evaluating it. The return value of a macro is a Lisp list that's treated as code. The original place with the macro is replaced with this code. For example, we could define a macro plus that takes two arguments and puts in the code that adds them.

What does it have to do with metaprogramming and our to-do list problem? Well, for one, macros are little bits of code that generate code using a list abstraction. Also, we could create macros named to-do and item that replace our data with whatever code we like, for instance code that displays the item to the user.

What benefits does this approach offer? We don't have to walk the list. The compiler will do it for us and will invoke appropriate macros. All we need to do is create the macros that convert our data to appropriate code!

For example, a macro similar to our triple C macro we showed earlier looks like this:

(defmacro triple (x)
'(+ ~x ~x ~x))
The quote prevents evaluation while the tilde allows it. Now every time triple is encountered in lisp code:

(triple 4)
it is replaced with the following code:

(+ 4 4 4)
We can create macros for our to-do list items that will get called by lisp compiler and will transform the to-do list into code. Now our to-do list will be treated as code and will be executed. Suppose all we want to do is print it to standard output for the user to read:

(defmacro item (priority note)
'(block
(print stdout tab "Priority: "
~(head (tail priority)) endl)
(print stdout tab "Note: " ~note endl endl)))
We've just created a very small and limited language for managing to-do lists embedded in Lisp. Such languages are very specific to a particular problem domain and are often referred to as domain specific languages or DSLs.

Domain Specific Languages
In this article we've already encountered two domain specific languages: Ant (specific to dealing with project builds) and our unnamed mini-language for dealing with to-do lists. The difference is that Ant was written from scratch using XML, an XML parser, and Java while our language is embedded into Lisp and is easily created within a couple of minutes.

We've already discussed the benefits of DSLs, mainly why Ant is using XML, not Java source code. Lisp lets us create as many DSLs as we need for our problem. We can create domain specific languages for creating web applications, writing massively multiplayer games, doing fixed income trading, solving the protein folding problem, dealing with transactions, etc. We can layer these languages on top of each other and create a language for writing web-based trading applications by taking advantage of our web application language and bond trading language. Every day we'd reap the benefits of this approach, much like we reap the benefits of Ant.

Using DSLs to solve problems results in much more compact, maintainable, flexible programs. In a way we create them in Java by creating classes that help us solve the problem. The difference is that Lisp allows us to take this abstraction to the next level: we're not limited by Java's parser. Think of writing build scripts in Java itself using some supporting library. Compare it to using Ant. Now apply this same comparison to every single problem you've ever worked on and you'll begin to glimpse a small share of the benefits offered by Lisp.

What's next?
Learning Lisp is an uphill battle. Even though in Computer Science terms Lisp is an ancient language, few people to date figured out how to teach it well enough to make it accessible. Despite great efforts by many Lisp advocates, learning Lisp today is still hard. The good news is that this won't remain the case forever since the amount of Lisp-related resources is rapidly increasing. Time is on Lisp's side.

Lisp is a way to escape mediocrity and to get ahead of the pack. Learning Lisp means you can get a better job today, because you can impress any reasonably intelligent interviewer with fresh insight into most aspects of software engineering. It also means you're likely to get fired tomorrow because everyone is tired of you constantly mentioning how much better the company could be doing if only its software was written in Lisp. Is it worth the effort? Everyone who has ever learned Lisp says yes. The choice, of course, remains yours.

Comments?
Whew. That's enough. I've been writing this article, on and off, for months. If you find it interesting, have any questions, comments, or suggestions, please drop a note at coffeemug@gmail.com. I'll be glad to hear your feedback.

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.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末勇垛,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子士鸥,更是在濱河造成了極大的恐慌闲孤,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烤礁,死亡現(xiàn)場離奇詭異崭放,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鸽凶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門币砂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人玻侥,你說我怎么就攤上這事决摧。” “怎么了凑兰?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵掌桩,是天一觀的道長。 經(jīng)常有香客問我姑食,道長波岛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任音半,我火速辦了婚禮则拷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘曹鸠。我一直安慰自己煌茬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布彻桃。 她就那樣靜靜地躺著坛善,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上眠屎,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天剔交,我揣著相機(jī)與錄音,去河邊找鬼改衩。 笑死岖常,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的燎字。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼阿宅,長吁一口氣:“原來是場噩夢啊……” “哼候衍!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起洒放,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤蛉鹿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后往湿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妖异,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年领追,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了他膳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绒窑,死狀恐怖棕孙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情些膨,我是刑警寧澤蟀俊,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站订雾,受9級特大地震影響肢预,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜洼哎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一烫映、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧噩峦,春花似錦窑邦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至李请,卻和暖如春瞧筛,著一層夾襖步出監(jiān)牢的瞬間厉熟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工较幌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留揍瑟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓乍炉,卻偏偏與公主長得像绢片,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子岛琼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容

  • rljs by sennchi Timeline of History Part One The Cognitiv...
    sennchi閱讀 7,322評論 0 10
  • 小丑的夢無人能懂槐瑞,小丑的愛還會有誰能懂熙涤,多希望這是一場夢,更希望它是一場不用醒來的夢 我愿意花永恒不醒的時(shí)間來兌現(xiàn)...
    冰冰的小耳朵閱讀 431評論 0 0
  • 朱欣怡閱讀 215評論 4 3
  • 始于顏值困檩,陷于才華祠挫,終于人品。我愛的樣子你都有悼沿,你示人的表象我都愛等舔。MIKE,真心希望你越來越好糟趾,連同你身邊最親近...
    年少模樣閱讀 225評論 0 0
  • 今天醫(yī)生下了醫(yī)囑了软瞎,明天一早開始催產(chǎn)了,如果明后天不能順產(chǎn)拉讯,就要給我剖腹產(chǎn)了涤浇,希望你跟媽媽一起加油!一定要順利出來...
    等等麻閱讀 232評論 0 0