溫馨提示×

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

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

Lisp的本質(zhì)是什么

發(fā)布時(shí)間:2022-03-19 10:16:35 來源:億速云 閱讀:161 作者:iii 欄目:云計(jì)算

這篇“Lisp的本質(zhì)是什么”文章的知識(shí)點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“Lisp的本質(zhì)是什么”文章吧。

重新審視XML

千里之行始于足下。讓我們的第一步從XML開始??墒荴ML已經(jīng)說得更多的了, 還能有什么新意思可說呢? 有的。XML自身雖然談?wù)劜簧嫌腥? 但是XML和Lisp的關(guān)系卻相當(dāng)有趣。XML和Lisp的概念有著驚人的相似之處。XML是我們通向理解Lisp的橋梁。好吧, 我們且把XML當(dāng)作活馬醫(yī)。讓我們拿好手杖, 對(duì)XML的無人涉及的荒原地帶作一番探險(xiǎn)。我們要從一個(gè)全新的視角來考察這個(gè)題目。

表面上看, XML是一種標(biāo)準(zhǔn)化語法, 它以適合人閱讀的格式來表達(dá)任意的層次化數(shù)據(jù)(hirearchical data)。象任務(wù)表(to-do list), 網(wǎng)頁, 病歷, 汽車保險(xiǎn)單, 配置文件等等, 都是XML用武的地方。比如我們拿任務(wù)表做例子:

<todo name="housework">

<item priority="high">Clean the house.</item>

<item priority="medium">Wash the dishes.</item>

<item priority="medium">Buy more soap.</item>

</todo>

解析這段數(shù)據(jù)時(shí)會(huì)發(fā)生什么情況? 解析之后的數(shù)據(jù)在內(nèi)存中怎樣表示? 顯然, 用樹來表示這種層次化數(shù)據(jù)是很恰當(dāng)?shù)?。說到底, XML這種比較容易閱讀的數(shù)據(jù)格式, 就是樹型結(jié)構(gòu)數(shù)據(jù)經(jīng)過序列化之后的結(jié)果。任何可以用樹來表示的數(shù)據(jù), 同樣可以用XML來表示, 反之亦然。希望你能懂得這一點(diǎn), 這對(duì)下面的內(nèi)容極其重要。

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

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

int add(int arg1, int arg2)

{

return arg1+arg2;

}

能把這個(gè)函數(shù)變成對(duì)等的XML格式嗎? 當(dāng)然可以。我們可以用很多種方式做到, 下面是其中的一種, 十分簡(jiǎn)單:

<define-function return-type="int" name="add">

<arguments>

<argument type="int">arg1</argument>

<argument type="int">arg2</argument>

</arguments>

<body>

<return>

<add value1="arg1" value2="arg2" />

</return>

</body>

</define>

這個(gè)例子非常簡(jiǎn)單, 用哪種語言來做都不會(huì)有太大問題。我們可以把任何程序碼轉(zhuǎn)成XML,也可以把XML轉(zhuǎn)回到原來的程序碼。我們可以寫一個(gè)轉(zhuǎn)換器, 把Java代碼轉(zhuǎn)成XML, 另一個(gè)轉(zhuǎn)換器把XML轉(zhuǎn)回到Java。一樣的道理, 這種手段也可以用來對(duì)付C++(這樣做跟發(fā)瘋差不多么??墒堑拇_有人在做, 看看GCC-XML(http://www.gccxml.org)就知道了)。進(jìn)一步說,凡是有相同語言特性而語法不同的語言, 都可以把XML當(dāng)作中介來互相轉(zhuǎn)換代碼。實(shí)際上幾乎所有的主流語言都在一定程度上滿足這個(gè)條件。我們可以把XML作為一種中間表示法,在兩種語言之間互相譯碼。比方說, 我們可以用Java2XML把Java代碼轉(zhuǎn)換成XML, 然后用XML2CPP再把XML轉(zhuǎn)換成C++代碼, 運(yùn)氣好的話, 就是說, 如果我們小心避免使用那些C++不具備的Java特性的話, 我們可以得到完好的C++程序。這辦法怎么樣, 漂亮吧?

這一切充分說明, 我們可以把XML作為源代碼的通用存儲(chǔ)方式, 其實(shí)我們能夠產(chǎn)生一整套使用統(tǒng)一語法的程序語言, 也能寫出轉(zhuǎn)換器, 把已有代碼轉(zhuǎn)換成XML格式。如果真的采納這種辦法, 各種語言的編譯器就用不著自己寫語法解析了, 它們可以直接用XML的語法解析來直接生成抽象語法樹。

說到這里你該問了, 我們研究了這半天XML, 這和Lisp有什么關(guān)系呢? 畢竟XML出來之時(shí),Lisp早已經(jīng)問世三十年了。這里我可以保證, 你馬上就會(huì)明白。不過在繼續(xù)解釋之前, 我們先做一個(gè)小小的思維練習(xí)??匆幌律厦孢@個(gè)XML版本的add函數(shù)例子, 你怎樣給它分類,是代碼還是數(shù)據(jù)? 不用太多考慮都能明白, 把它分到哪一類都講得通。它是XML, 它是標(biāo)準(zhǔn)格式的數(shù)據(jù)。我們也知道, 它可以通過內(nèi)存中的樹結(jié)構(gòu)來生成(GCC-XML做的就是這個(gè)事情)。它保存在不可執(zhí)行的文件中。我們可以把它解析成樹節(jié)點(diǎn), 然后做任意的轉(zhuǎn)換。顯而易見, 它是數(shù)據(jù)。不過且慢, 雖然它語法有點(diǎn)陌生, 可它又確確實(shí)實(shí)是一個(gè)add函數(shù),對(duì)吧?  一旦經(jīng)過解析, 它就可以拿給編譯器編譯執(zhí)行。我們可以輕而易舉寫出這個(gè)XML代碼解釋器, 并且直接運(yùn)行它?;蛘呶覀円部梢园阉g成Java或C++代碼, 然后再編譯運(yùn)行。所以說, 它也是代碼。

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

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

重新審視Ant

我們現(xiàn)在已經(jīng)來到了月亮背光的那一面, 先別忙著離開。再探索一下, 看看我們還能發(fā)現(xiàn)什么東西。閉上眼睛, 想一想2000年冬天的那個(gè)雨夜, 一個(gè)名叫James Duncan Davidson的杰出的程序員正在研究Tomcat的servlet容器。那時(shí), 他正小心地保存好剛修改過的文件, 然后執(zhí)行make。結(jié)果冒出了一大堆錯(cuò)誤, 顯然有什么東西搞錯(cuò)了。經(jīng)過仔細(xì)檢查, 他想, 難道是因?yàn)閠ab前面加了個(gè)空格而導(dǎo)致命令不能執(zhí)行嗎? 確實(shí)如此。老是這樣, 他真的受夠了。烏云背后的月亮給了他啟示, 他創(chuàng)建了一個(gè)新的Java項(xiàng)目, 然后寫了一個(gè)簡(jiǎn)單但是十分有用的工具, 這個(gè)工具巧妙地利用了Java屬性文件中的信息來構(gòu)造工程, 現(xiàn)在James可以寫makefile的替代品, 它能起到相同的作用, 而形式更加優(yōu)美, 也不用擔(dān)心有makefile那樣可恨的空格問題。這個(gè)工具能夠自動(dòng)解釋屬性文件, 然后采取正確的動(dòng)作來編譯工程。真是簡(jiǎn)單而優(yōu)美。

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

使用Ant構(gòu)造Tomcat之后幾個(gè)月, 他越來越感到Java的屬性文件不足以表達(dá)復(fù)雜的構(gòu)造指令。文件需要檢出, 拷貝, 編譯, 發(fā)到另外一臺(tái)機(jī)器, 進(jìn)行單元測(cè)試。要是出錯(cuò), 就發(fā)郵件給相關(guān)人員, 要是成功, 就繼續(xù)在盡可能高層的卷(volumn)上執(zhí)行構(gòu)造。追蹤到最后,卷要回復(fù)到最初的水平上。確實(shí), Java的屬性文件不夠用了, James需要更有彈性的解決方案。他不想自己寫解析器(因?yàn)樗M幸粋€(gè)具有工業(yè)標(biāo)準(zhǔn)的方案)。XML看起來是個(gè)不錯(cuò)的選擇。他花了幾天工夫把Ant移植到XML,于是,一件偉大的工具誕生了。

Ant是怎樣工作的?原理非常簡(jiǎn)單。Ant把包含有構(gòu)造命令的XML文件(算代碼還是算數(shù)據(jù),你自己想吧),交給一個(gè)Java程序來解析每一個(gè)元素,實(shí)際情況比我說的還要簡(jiǎn)單得多。一個(gè)簡(jiǎn)單的XML指令會(huì)導(dǎo)致具有相同名字的Java類裝入,并執(zhí)行其代碼。

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

<fileset dir="src_dir" />

</copy>

這段文字的含義是把源目錄復(fù)制到目標(biāo)目錄,Ant會(huì)找到一個(gè)”copy”任務(wù)(實(shí)際上就是一個(gè)Java類), 通過調(diào)用Java的方法來設(shè)置適當(dāng)參數(shù)(todir和fileset),然后執(zhí)行這個(gè)任務(wù)。Ant帶有一組核心類, 可以由用戶任意擴(kuò)展, 只要遵守若干約定就可以。Ant找到這些類,每當(dāng)遇到XML元素有同樣的名字, 就執(zhí)行相應(yīng)的代碼。過程非常簡(jiǎn)單。Ant做到了我們前面所說的東西: 它是一個(gè)語言解釋器, 以XML作為語法, 把XML元素轉(zhuǎn)譯為適當(dāng)?shù)腏ava指令。我們可以寫一個(gè)”add”任務(wù), 然后, 當(dāng)發(fā)現(xiàn)XML中有add描述的時(shí)候, 就執(zhí)行這個(gè)add任務(wù)。由于Ant是非常流行的項(xiàng)目, 前面展示的策略就顯得更為明智。畢竟, 這個(gè)工具每天差不多有幾千家公司在使用。

到目前為之, 我還沒有說Ant在解析XML時(shí)所遇到困難。你也不用麻煩去它的網(wǎng)站上去找答案了, 不會(huì)找到有價(jià)值的東西。至少對(duì)我們這個(gè)論題來說是如此。我們還是繼續(xù)下一步討論吧。我們答案就在那里。

為什么是XML

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

有的。雖然我不確定James是否確實(shí)意識(shí)到了。在語義的可構(gòu)造性方面, XML的彈性是Java望塵莫及的。我不想用高深莫測(cè)的名詞來嚇唬你, 其中的道理相當(dāng)簡(jiǎn)單, 解釋起來并不費(fèi)很多功夫。好, 做好預(yù)備動(dòng)作, 我們馬上就要朝向頓悟的時(shí)刻做奮力一躍。

上面的那個(gè)copy的例子, 用Java代碼怎樣實(shí)現(xiàn)呢? 我們可以這樣做:

CopyTask copy = new CopyTask();

Fileset fileset = new Fileset();

fileset.setDir("src_dir");

copy.setToDir("../new/dir");

copy.setFileset(fileset);

copy.excute();

這個(gè)代碼看起來和XML的那個(gè)很相似, 只是稍微長(zhǎng)一點(diǎn)。差別在那里? 差別在于XML構(gòu)造了一個(gè)特殊的copy動(dòng)詞, 如果我們硬要用Java來寫的話, 應(yīng)該是這個(gè)樣子:

copy("../new/dir");

{

   fileset("src_dir");

}

看到差別了嗎? 以上代碼(如果可以在Java中用的化), 是一個(gè)特殊的copy算符, 有點(diǎn)像for循環(huán)或者Java5中的foreach循環(huán)。如果我們有一個(gè)轉(zhuǎn)換器, 可以把XML轉(zhuǎn)換到Java, 大概就會(huì)得到上面這段事實(shí)上不可以執(zhí)行的代碼。因?yàn)镴ava的技術(shù)規(guī)范是定死的, 我們沒有辦法在程序里改變它。我們可以增加包, 增加類, 增加方法, 但是我們沒辦法增加算符,而對(duì)于XML, 我們顯然可以任由自己增加這樣的東西。對(duì)于XML的語法樹來說, 只要原意,我們可以任意增加任何元素, 因此等于我們可以任意增加算符。如果你還不太明白的話,看下面這個(gè)例子, 加入我們要給Java引入一個(gè)unless算符:

unless(someObject.canFly())

{

   someObject.transportByGround():

}

在上面的兩個(gè)例子中, 我們打算給Java語法擴(kuò)展兩個(gè)算符, 成組拷貝文件算符和條件算符unless, 我們要想做到這一點(diǎn), 就必須修改Java編譯器能夠接受的抽象語法樹, 顯然我們無法用Java標(biāo)準(zhǔn)的功能來實(shí)現(xiàn)它。但是在XML中我們可以輕而易舉地做到。我們的解析器根據(jù) XML元素, 生成抽象語法樹, 由此生成算符, 所以, 我們可以任意引入任何算符。

對(duì)于復(fù)雜的算符來說, 這樣做的好處顯而易見。比如, 用特定的算符來做檢出源碼, 編譯文件, 單元測(cè)試, 發(fā)送郵件等任務(wù), 想想看有多么美妙。對(duì)于特定的題目, 比如說構(gòu)造軟件項(xiàng)目, 這些算符的使用可以大幅減低少代碼的數(shù)量。增加代碼的清晰程度和可重用性。解釋性的XML可以很容易的達(dá)到這個(gè)目標(biāo)。XML是存儲(chǔ)層次化數(shù)據(jù)的簡(jiǎn)單數(shù)據(jù)文件, 而在Java中, 由于層次結(jié)構(gòu)是定死的(你很快就會(huì)看到, Lisp的情況與此截然不同), 我們就沒法達(dá)到上述目標(biāo)。也許這正是Ant的成功之處呢。

你可以注意一下最近Java和C#的變化(尤其是C#3.0的技術(shù)規(guī)范), C#把常用的功能抽象出來, 作為算符增加到C#中。C#新增加的query算符就是一個(gè)例子。它用的還是傳統(tǒng)的作法:C#的設(shè)計(jì)者修改抽象語法樹, 然后增加對(duì)應(yīng)的實(shí)現(xiàn)。如果程序員自己也能修改抽象語法樹該有多好! 那樣我們就可以構(gòu)造用于特定問題的子語言(比如說就像Ant這種用于構(gòu)造項(xiàng)目的語言), 你能想到別的例子嗎? 再思考一下這個(gè)概念。不過也不必思考太甚, 我們待會(huì)還會(huì)回到這個(gè)題目。那時(shí)候就會(huì)更加清晰。

離Lisp越來越近

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

<task name="Test">

<echo message="Hello World" />

</task>

<Test />

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

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

比XML更好

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

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

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

<fileset dir="src_dir">

</copy>

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

<copy>

<toDir>../new/dir</toDir>

<fileset>

<dir>src_dir</dir>

</fileset>

</copy>

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

說完了屬性的問題, 我們?cè)賮砜匆豢磗表達(dá)式。之所以繞這么個(gè)彎, 是因?yàn)閟表達(dá)式?jīng)]有屬性的概念。因?yàn)閟表達(dá)式非常簡(jiǎn)練, 根本沒有必要引入屬性。我們?cè)诎裍ML轉(zhuǎn)換成s表達(dá)式的時(shí)候, 心里應(yīng)該記住這一點(diǎn)??磦€(gè)例子, 上面的代碼譯成s表達(dá)式是這樣的:

(copy

(todir "../new/dir")

(fileset (dir "src_dir")))

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

(task (name "Test")

(echo (message "Hellow World!")))

(Test)

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

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

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

經(jīng)受這一番折磨以后, 現(xiàn)在我們終于相當(dāng)接近Lisp了, Lisp的括號(hào)的神秘本質(zhì)(就像許多Lisp狂熱分子認(rèn)為的)逐漸顯現(xiàn)出來?,F(xiàn)在我們繼續(xù)研究其他內(nèi)容。

重新審視C語言的宏

到了這里, 對(duì)XML的討論你大概都聽累了, 我都講累了。我們先停一停, 把樹, s表達(dá)式,Ant這些東西先放一放, 我們來說說C的預(yù)處理器。一定有人問了, 我們的話題和C有什么關(guān)系? 我們已經(jīng)知道了很多關(guān)于元編程的事情, 也探討過專門寫代碼的代碼。理解這問題有一定難度, 因?yàn)橄嚓P(guān)討論文章所使用的編程語言, 都是你們不熟悉的。但是如果只論概念的話, 就相對(duì)要簡(jiǎn)單一些。我相信, 如果以C語言做例子來討論元編程, 理解起來一定會(huì)容易得多。好, 我們接著看。

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

概念挺簡(jiǎn)單, 其要點(diǎn)是把數(shù)據(jù)訪問的內(nèi)容(至少是那些比較瑣碎的部分)抽象出來, 用類來映射數(shù)據(jù)庫的表, 然后用訪問對(duì)象屬性訪問器(accessor)的辦法來間接實(shí)現(xiàn)查詢。這樣就極大地簡(jiǎn)化了開發(fā)工作量。我們用訪問對(duì)象的方法(或者屬性賦值, 這要視你選用的語言而定)來代替寫SQL查詢語句。凡是用過這種方法的人, 都知道這很節(jié)省時(shí)間。當(dāng)然, 如果你要親自寫這樣一個(gè)抽象層, 那可是要花非常多的時(shí)間的–你要寫一組類來映射表, 把屬性訪問轉(zhuǎn)換為SQL查詢, 這個(gè)活相當(dāng)耗費(fèi)精力。用手工來做顯然是很不明智的。但是一旦你有了方案和模板, 實(shí)際上就沒有多少東西需要思考的。你只需要按照同樣的模板一次又一次重復(fù)編寫相似代碼就可以了。事實(shí)上很多人已經(jīng)發(fā)現(xiàn)了更好的方法, 有一些工具可以幫助你連接數(shù)據(jù)庫, 抓取數(shù)據(jù)庫結(jié)構(gòu)定義(schema), 按照預(yù)定義的或者用戶定制的模板來自動(dòng)編寫代碼。

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

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

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

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

#define triple(X) X+X+X

這一行的作用是什么? 這是一個(gè)簡(jiǎn)單的預(yù)編譯指令, 它把程序中的triple(X)替換稱為X+X+X。例如, 把所有的triple(5)都換成5+5+5, 然后再交給編譯器編譯。這就是一個(gè)簡(jiǎn)單的代碼生成的例子。要是C的預(yù)處理器再強(qiáng)大一點(diǎn), 要是能夠允許連接數(shù)據(jù)庫, 要是能多一些其他簡(jiǎn)單的機(jī)制, 我們就可以在我們程序的內(nèi)部開發(fā)自己的數(shù)據(jù)訪問層。下面這個(gè)例子, 是一個(gè)假想的對(duì)C宏的擴(kuò)展:

#get-db-schema("127.0.0.1")

#iterate-through-tables

#for-each-table

class #table-name

{

};

#end-for-each

我們連接數(shù)據(jù)庫結(jié)構(gòu)定義, 遍歷數(shù)據(jù)表, 然后對(duì)每個(gè)表創(chuàng)建一個(gè)類, 只消幾行代碼就完成了這個(gè)工作。這樣每次編譯工程的時(shí)候, 這些類都會(huì)根據(jù)數(shù)據(jù)庫的定義同步更新。顯而易見, 我們不費(fèi)吹灰之力就在程序內(nèi)部建立了一個(gè)完整的數(shù)據(jù)訪問層, 根本用不著任何外部工具。當(dāng)然這種作法有一個(gè)缺點(diǎn), 那就是我們得學(xué)習(xí)一套新的”編譯時(shí)語言”, 另一個(gè)缺點(diǎn)就是根本不存在這么一個(gè)高級(jí)版的C預(yù)處理器。需要做復(fù)雜代碼生成的時(shí)候, 這個(gè)語言(譯者注: 這里指預(yù)處理指令, 即作者所說的”編譯時(shí)語言”)本身也一定會(huì)變得相當(dāng)復(fù)雜。它必須支持足夠多的庫和語言結(jié)構(gòu)。比如說我們想要生成的代碼要依賴某些ftp服務(wù)器上的文件, 預(yù)處理器就得支持ftp訪問, 僅僅因?yàn)檫@個(gè)任務(wù)而不得不創(chuàng)造和學(xué)習(xí)一門新的語言,真是有點(diǎn)讓人惡心(事實(shí)上已經(jīng)存在著有此能力的語言, 這樣做就更顯荒謬)。我們不妨再靈活一點(diǎn), 為什么不直接用 C/C++自己作為自己的預(yù)處理語言呢?  這樣子的話, 我們可以發(fā)揮語言的強(qiáng)大能力, 要學(xué)的新東西也只不過是幾個(gè)簡(jiǎn)單的指示字 , 這些指示字用來區(qū)別編譯時(shí)代碼和運(yùn)行時(shí)代碼。

<%

cout<<"Enter a number: ";

cin>>n;

%>

for(int i=0;i< <% n %>;i++)

{

cout<<"hello"<<endl;

}

你明白了嗎? 在<%和%>標(biāo)記之間的代碼是在編譯時(shí)運(yùn)行的, 標(biāo)記之外的其他代碼都是普通代碼。編譯程序時(shí), 系統(tǒng)會(huì)提示你輸入一個(gè)數(shù), 這個(gè)數(shù)在后面的循環(huán)中會(huì)用到。而for循環(huán)的代碼會(huì)被編譯。假定你在編譯時(shí)輸入5, for循環(huán)的代碼將會(huì)是:

for(int i=0;i<5; i++)

{

   cout<<"hello"<<endl;

}

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

以上就是關(guān)于“Lisp的本質(zhì)是什么”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道。

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

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

AI