溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

怎么理解Lisp的本質(zhì)

發(fā)布時間:2021-11-17 17:00:17 來源:億速云 閱讀:99 作者:iii 欄目:web開發(fā)

本篇內(nèi)容介紹了“怎么理解Lisp的本質(zhì)”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

簡介

最初在web的某些角落偶然看到有人贊美Lisp時, 我那時已經(jīng)是一個頗有經(jīng)驗的程序員。在我的履歷上, 掌握的語言范圍相當(dāng)廣泛, 象C++, Java, C#主流語言等等都不在話下,我覺得我差不多知道所有的有關(guān)編程語言的事情。對待編程語言的問題上, 我覺得自己不太會遇到什么大問題。其實我大錯特錯了。

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

怎么理解Lisp的本質(zhì)

回過神來之后, 我和Lisp社區(qū)的那伙人交談, 訴說我的沮喪心情。結(jié)果, 立馬就有一大套理論砸過來, 這套理論在Lisp社區(qū)處處可見, 幾成慣例。比如說: Lisp的括號只是表面現(xiàn)象; Lisp的代碼和數(shù)據(jù)的表達(dá)方式?jīng)]有差別, 而且比XML語法高明許多, 所以有無窮的好處; Lisp有強大無比的元語言能力, 程序員可以寫出自我維護的代碼; Lisp可以創(chuàng)造出針對特定應(yīng)用的語言子集; Lisp的運行時和編譯時沒有明確的分界; 等等, 等等, 等等。這么長的贊美詞雖然看起來相當(dāng)動人, 不過對我毫無意義。沒人能給我演示這些東西是如何應(yīng)用的, 因為這些東西一般來說只有在大型系統(tǒng)才會用到。我爭辯說, 這些東西傳統(tǒng)語言一樣辦得到。在和別人爭論了數(shù)個小時之后, 我最終還是放棄了學(xué)Lisp的念頭。為什么要花費幾個月的時間學(xué)習(xí)語法這么難看的語言呢? 這種語言的概念這么晦澀, 又沒什么好懂的例子。也許這語言不是該我這樣的人學(xué)的。

幾個月來, 我承受著這些Lisp辯護士對我心靈的重壓。我一度陷入了困惑。我認(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ù)?。說到底, XML這種比較容易閱讀的數(shù)據(jù)格式, 就是樹型結(jié)構(gòu)數(shù)據(jù)經(jīng)過序列化之后的結(jié)果。任何可以用樹來表示的數(shù)據(jù), 同樣可以用XML來表示, 反之亦然。希望你能懂得這一點, 這對下面的內(nèi)容極其重要。

再進一步。還有什么類型的數(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)就知道了)。進一步說,凡是有相同語言特性而語法不同的語言, 都可以把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代碼解釋器, 并且直接運行它?;蛘呶覀円部梢园阉g成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ā)到另外一臺機器, 進行單元測試。要是出錯, 就發(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的一切好處, 這些好處是我們剛剛探討過的?,F(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)然, 如果你要親自寫這樣一個抽象層, 那可是要花非常多的時間的&ndash;你要寫一組類來映射表, 把屬性訪問轉(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é)省時間?,F(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")  #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中, 我們給表加一個前綴&rsquo;來表示數(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變換和制作代碼生成工具?;蛘呶覀円部梢杂肔isp來做這項工作。正像我們以前所知的, 表是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ù)展示給用戶?,F(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解決問題, 做出的程序精簡, 易于維護, 富有彈性。在Java里面, 我們可以用類來處理問題。這兩種方法的差別在于, Lisp使我們達(dá)到了一個更高層次的抽象, 我們不再受語言解析器本身的限制, 比較一下用Java庫直接寫的構(gòu)造腳本和用Ant寫的構(gòu)造腳本其間的差別。同樣的, 比較一下你以前所做的工作, 你就會明白Lisp帶來的好處。

“怎么理解Lisp的本質(zhì)”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI