老熟女激烈的高潮_日韩一级黄色录像_亚洲1区2区3区视频_精品少妇一区二区三区在线播放_国产欧美日产久久_午夜福利精品导航凹凸

重慶分公司,新征程啟航

為企業(yè)提供網站建設、域名注冊、服務器等服務

怎么理解Lisp的本質

本篇內容介紹了“怎么理解Lisp的本質”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

創(chuàng)新互聯公司-專業(yè)網站定制、快速模板網站建設、高性價比輝縣網站開發(fā)、企業(yè)建站全套包干低至880元,成熟完善的模板庫,直接使用。一站式輝縣網站制作公司更省心,省錢,快速模板網站建設找我們,業(yè)務覆蓋輝縣地區(qū)。費用合理售后完善,十年實體公司更值得信賴。

簡介

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

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

怎么理解Lisp的本質

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

幾個月來, 我承受著這些Lisp辯護士對我心靈的重壓。我一度陷入了困惑。我認識一些絕頂聰明的人, 我對他們相當尊敬, 我看到他們對Lisp的贊美達到了宗教般的高度。這就是說, Lisp中一定有某種神秘的東西存在, 我不能忍受自己對此的無知, 好奇心和求知欲最終不可遏制。我于是咬緊牙關埋頭學習Lisp, 經過幾個月的時間費勁心力的練習, 終于,我看到了那無窮無盡的泉水的源頭。在經過脫胎換骨的磨練之后, 在經過七重地獄的煎熬之后, 終于, 我明白了。

頓悟在突然之間來臨。曾經許多次, 我聽到別人引用雷蒙德(譯者注: 論文<<大教堂和市集>>的作者, 著名的黑客社區(qū)理論家)的話: “Lisp語言值得學習。當你學會Lisp之后, 你會擁有深刻的體驗。就算你平常并不用Lisp編程, 它也會使你成為更加優(yōu)秀的程序員”。過去, 我根本不懂這些話的含義, 我也不相信這是真的。可是現在我懂得了。這些話蘊含的真理遠遠超過我過去的想像。我內心體會到一種神圣的情感, 一瞬間的頓悟, 幾乎使我對電腦科學的觀念發(fā)生了根本的改變。

頓悟的那一刻, 我成了Lisp的崇拜者。我體驗到了宗教大師的感受: 一定要把我的知識傳布開來, 至少要讓10個迷失的靈魂得到拯救。按照通常的辦法, 我把這些道理(就是剛開始別人砸過來的那一套, 不過現在我明白了真實的含義)告訴旁人。結果太令人失望了,只有少數幾個人在我堅持之下, 發(fā)生了一點興趣, 但是僅僅看了幾眼Lisp代碼, 他們就退卻了。照這樣的辦法, 也許費數年功夫能造就了幾個Lisp迷, 但我覺得這樣的結果太差強人意了, 我得想一套有更好的辦法。

我深入地思考了這個問題。是不是Lisp有什么很艱深的東西, 令得那么多老練的程序員都不能領會? 不是, 沒有任何絕對艱深的東西。因為我能弄懂, 我相信其他人也一定能。那么問題出在那里? 后來我終于找到了答案。我的結論就是, 凡是教人學高級概念, 一定要從他已經懂得的東西開始。如果學習過程很有趣, 學習的內容表達得很恰當, 新概念就會變得相當直觀。這就是我的答案。所謂元編程, 所謂數據和代碼形式合一, 所謂自修改代碼, 所謂特定應用的子語言, 所有這些概念根本就是同族概念, 彼此互為解釋, 肯定越講越不明白。還是從實際的例子出發(fā)最有用。

我把我的想法說給Lisp程序員聽, 遭到了他們的反對。”這些東西本身當然不可能用熟悉的知識來解釋, 這些概念完全與眾不同, 你不可能在別人已有的經驗里找到類似的東西”,可是我認為這些都是遁詞。他們又反問我, “你自己為啥不試一下?” 好吧, 我來試一下。這篇文章就是我嘗試的結果。我要用熟悉的直觀的方法來解釋Lisp, 我希望有勇氣的人讀完它, 拿杯飲料, 深呼吸一下, 準備被搞得暈頭轉向。來吧, 愿你獲得大能。

重新審視XML

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

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

  Clean the house.  Wash the dishes.  Buy more soap.  

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

再進一步。還有什么類型的數據也常用樹來表示? 無疑列表(list)也是一種。上過編譯課吧? 還模模糊糊記得一點吧? 源代碼在解析之后也是用樹結構來存放的, 任何編譯程序都會把源代碼解析成一棵抽象語法樹, 這樣的表示法很恰當, 因為源代碼就是層次結構的:函數包含參數和代碼塊, 代碼快包含表達式和語句, 語句包含變量和運算符等等。

我們已經知道, 任何樹結構都可以輕而易舉的寫成XML, 而任何代碼都會解析成樹, 因此,任何代碼都可以轉換成XML, 對不對? 我舉個例子, 請看下面的函數:

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

能把這個函數變成對等的XML格式嗎? 當然可以。我們可以用很多種方式做到, 下面是其中的一種, 十分簡單:

    arg1  arg2              

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

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

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

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

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

重新審視Ant

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

(作者注: 我不認識James, James也不認識我, 這個故事是根據網上關于Ant歷史的帖子虛構的)

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

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

    

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

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

為什么是XML

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

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

上面的那個copy的例子, 用Java代碼怎樣實現呢? 我們可以這樣做:

CopyTask copy = new CopyTask();  Fileset fileset = new Fileset();     fileset.setDir("src_dir");  copy.setToDir("../new/dir");  copy.setFileset(fileset);     copy.excute();

這個代碼看起來和XML的那個很相似, 只是稍微長一點。差別在那里? 差別在于XML構造了一個特殊的copy動詞, 如果我們硬要用Java來寫的話, 應該是這個樣子:

copy("../new/dir");  {      fileset("src_dir");  }

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

unless(someObject.canFly())  {      someObject.transportByGround():  }

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

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

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

離Lisp越來越近

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

      

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

再看一回我們剛才說的Task任務。祝賀你呀, 你在看Lisp代碼!!! 我說什么? 一點都不像Lisp嗎? 沒關系, 我們再給它收拾一下。

比XML更好

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

為了解決這個問題, 我們應當簡化寫法。須知, XML僅僅是一種表達層次化數據的方式。我們并不是一定要使用尖括號才能得到樹的序列化結果。我們完全可以采用其他的格式。其中的一種(剛好就是Lisp 所采用的)格式, 叫做s表達式。s表達式要做的和XML一樣, 但它的好處是寫法更簡單, 簡單的寫法更適合代碼輸入。后面我會詳細講s表達式。這之前我要清理一下XML的東西。考慮一下關于拷貝文件的例子:

    

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

  ../new/dir    src_dir    

兩下比較, 內容的信息量完全一樣, 用屬性可以減少打字數量。如果XML沒有屬性的話,光是打字就夠把人搞瘋掉。

說完了屬性的問題, 我們再來看一看s表達式。之所以繞這么個彎, 是因為s表達式沒有屬性的概念。因為s表達式非常簡練, 根本沒有必要引入屬性。我們在把XML轉換成s表達式的時候, 心里應該記住這一點。看個例子, 上面的代碼譯成s表達式是這樣的:

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

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

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

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

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

XML自然也可以用這樣的風格來寫。當然上面這句并不是一般意義上的元素表。它實際上是一個樹。這和XML的作用是一樣的。稱它為列表, 希望你不會感到迷惑, 因為嵌套表和樹實際上是一碼事。Lisp的字面意思就是表處理(list processing), 其實也可以稱為樹處理, 這和處理XML節(jié)點沒有什么不同。

經受這一番折磨以后, 現在我們終于相當接近Lisp了, Lisp的括號的神秘本質(就像許多Lisp狂熱分子認為的)逐漸顯現出來。現在我們繼續(xù)研究其他內容。

重新審視C語言的宏

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

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

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

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

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

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

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

#define triple(X) X+X+X

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

#get-db-schema("127.0.0.1")  #iterate-through-tables  #for-each-table  class #table-name  {  };  #end-for-each

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

<%  cout<<"Enter a number: ";  cin>>n;  %>  for(int i=0;i< <% n %>;i++)  {  cout<<"hello"<

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

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

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

你好, Lisp

到此刻為止, 我們所知的關于Lisp的指示可以總結為一句話: Lisp是一個可執(zhí)行的語法更優(yōu)美的XML, 但我們還沒有說Lisp是怎樣做到這一點的, 現在開始補上這個話題。

Lisp有豐富的內置數據類型, 其中的整數和字符串和其他語言沒什么分別。像71或者”hello”這樣的值, 含義也和C++或者Java這樣的語言大體相同。真正有意思的三種類型是符號(symbol), 表和函數。這一章的剩余部分, 我都會用來介紹這幾種類型, 還要介紹Lisp環(huán)境是怎樣編譯和運行源碼的。這個過程用Lisp的術語來說通常叫做求值。通讀這一節(jié)內容, 對于透徹理解元編程的真正潛力, 以及代碼和數據的同一性, 和面向領域語言的觀念, 都極其重要。萬勿等閑視之。我會盡量講得生動有趣一些, 也希望你能獲得一些啟發(fā)。那好, 我們先講符號。

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

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

好像有什么不對的地方? 假定我們對*賦給整數或者字符串值, 那做乘法時怎么辦? 不管怎么說, *總是乘法呀? 答案簡單極了。Lisp中函數的角色十分特殊, 函數也是一種數據類型, 就像整數和字符串一樣, 因此可以把它賦值給符號。乘法函數Lisp的內置函數, 默認賦給*, 你可以把其他函數賦值給*, 那樣*就不代表乘法了。你也可以把這函數的值存到另外的變量里。我們再用偽碼來說明一下:

3,4)          // 3乘4, 結果是12  set(temp, *)    // 把*的值, 也就是乘法函數, 賦值給temp  set(*, 3)       // 把3賦予*  *(3,4)          // 錯誤的表達式, *不再是乘法, 而是數值3  temp(3,4)       // temp是乘法函數, 所以此表達式的值為3乘4等于12  set(*, temp)    // 再次把乘法函數賦予*  *(3,4)          // 3乘4等于12

再古怪一點, 把減號的值賦給加號:

set(+, -)       // 減號(-)是內置的減法函數  +(5, 4)         // 加號(+)現在是代表減法函數, 結果是5減4等于1

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

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

這段代碼返回一個具有一個參數的函數, 函數的功能是計算參數乘2的結果。這個函數還沒有名字, 你可以把此函數賦值給別的符號:

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

我們現在可以這樣調用這個函數:

time-two(5)         // 返回10

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

()                      ; 空表  (1)                     ; 含一個元素的表  (1 "test")              ; 兩元素表, 一個元素是整數1, 另一個是字符串  (test "hello")          ; 兩元素表, 一個元素是符號, 另一個是字符串  (test (1 2) "hello")    ; 三元素表, 一個符號test, 一個含有兩個元素1和2的  ; 表, ***一個元素是字符串

當Lisp系統(tǒng)遇到這樣的表時, 它所做的, 和Ant處理XML數據所做的, 非常相似, 那就是試圖執(zhí)行它們。其實, Lisp源碼就是特定的一種表, 好比Ant源碼是一種特定的XML一樣。Lisp執(zhí)行表的順序是這樣的, 表的***個元素當作函數, 其他元素當作函數的參數。如果其中某個參數也是表, 那就按照同樣的原則對這個表求值, 結果再傳遞給最初的函數作為參數。這就是基本原則。我們看一下真正的代碼:

(* 3 4)                 ; 相當于前面列舉過的偽碼*(3,4), 即計算3乘4 (times-two 5)           ; 返回10, times-two按照前面的定義是求參數的2倍  (3 4)                   ; 錯誤, 3不是函數  (time-two)              ; 錯誤, times-two要求一個參數  (times-two 3 4)         ; 錯誤, times-two只要求一個參數  (set + -)               ; 把減法函數賦予符號+  (+ 5 4)                 ; 依據上一句的結果, 此時+表示減法, 所以返回1 (* 3 (+ 2 2))           ; 2+2的結果是4, 再乘3, 結果是12

上述的例子中, 所有的表都是當作代碼來處理的。怎樣把表當作數據來處理呢? 同樣的,設想一下, Ant是把XML數據當作自己的參數。在Lisp中, 我們給表加一個前綴’來表示數據。

(set test '(1 2))       ; test的值為兩元素表  (set test (1 2))        ; 錯誤, 1不是函數  (set test '(* 3 4))     ; test的值是三元素表, 三個元素分別是*, 3, 4

我們可以用一個內置的函數head來返回表的***個元素, tail函數來返回剩余元素組成的表。

(head '(* 3 4))         ; 返回符號*  (tail '(* 3 4))         ; 返回表(3 4)  (head (tal '(* 3 4)))   ; 返回3 (head test)             ; 返回*

你可以把Lisp的內置函數想像成Ant的任務。差別在于, 我們不用在另外的語言中擴展Lisp(雖然完全可以做得到), 我們可以用Lisp自己來擴展自己, 就像上面舉的times-two函數的例子。Lisp的內置函數集十分精簡, 只包含了十分必要的部分。剩下的函數都是作為標準庫來實現的。

Lisp宏

我們已經看到, 元編程在一個類似jsp的模板引擎方面的應用。我們通過簡單的字符串處理來生成代碼。但是我們可以做的更好。我們先提一個問題, 怎樣寫一個工具, 通過查找目錄結構中的源文件來自動生成Ant腳本。

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

我們當然可以直接用Ant自身來處理XML變換和制作代碼生成工具。或者我們也可以用Lisp來做這項工作。正像我們以前所知的, 表是Lisp內置的數據結構, Lisp含有大量的工具來快速有效的操作表(head和tail是最簡單的兩個)。而且, Lisp沒有語義約束, 你可以構造任何數據結構, 只要你原意。

Lisp通過宏(macro)來做元編程。我們寫一組宏來把任務列表(to-do list)轉換為專用領域語言。

回想一下上面to-do list的例子, 其XML的數據格式是這樣的:

  Clean the hose  Wash the dishes  Buy more soap  

相應的s表達式是這樣的:

(todo "housework" (item (priority high) "Clean the house")  (item (priority medium) "Wash the dishes")  (item (priority medium) "Buy more soap"))

假設我們要寫一個任務表的管理程序, 把任務表數據存到一組文件里, 當程序啟動時, 從文件讀取這些數據并顯示給用戶。在別的語言里(比如說Java), 這個任務該怎么做? 我們會解析XML文件, 從中得出任務表數據, 然后寫代碼遍歷XML樹, 再轉換為Java的數據結構(老實講, 在Java里解析XML真不是件輕松的事情), ***再把數據展示給用戶。現在如果用Lisp, 該怎么做?

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

但是還有更好的辦法。我們甚至不用寫表達式來存儲數據, 我們可以寫宏, 把數據當作代碼來處理。那該怎么做呢? 真的簡單。回想一下, Lisp的函數調用格式:

(function-name arg1 arg2 arg3)

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

(macro-name (+ 4 5))

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

這和元編程, 以及我們要做的任務表程序有什么關系呢? 實際上, 編譯器會替我們工作,調用相應的宏。我們所要做的, 僅僅是創(chuàng)建一個把數據轉換為適當代碼的宏。

例如, 上面曾經將過的C的求三次方的宏, 用Lisp來寫是這樣子:

(defmacro triple (x)  `(+ ~x ~x ~x))

(譯注: 在Common Lisp中, 此處的單引號應當是反單引號, 意思是對表不求值, 但可以對表中某元素求值, 記號~表示對元素x求值, 這個求值記號在Common Lisp中應當是逗號。反單引號和單引號的區(qū)別是, 單引號標識的表, 其中的元素都不求值。這里作者所用的記號是自己發(fā)明的一種Lisp方言Blaise, 和common lisp略有不同, 事實上, 發(fā)明方言是lisp高手獨有的樂趣, 很多狂熱分子都熱衷這樣做。比如Paul Graham就發(fā)明了ARC, 許多記號比傳統(tǒng)的Lisp簡潔得多, 顯得比較現代)

單引號的用處是禁止對表求值。每次程序中出現triple的時候,

(triple 4)

都會被替換成:

(+ 4 4 4)

我們可以為任務表程序寫一個宏, 把任務數據轉換為可執(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中的任務表。這個語言只用來解決特定領域的問題, 通常稱之為DSLs(特定領域語言, 或專用領域語言)。

特定領域語言

本文談到了兩個特定領域語言, 一個是Ant, 處理軟件構造。一個是沒起名字的, 用于處理任務表。兩者的差別在于, Ant是用XML, XML解析器, 以及Java語言合在一起構造出來的。而我們的迷你語言則完全內嵌在Lisp中, 只消幾分鐘就做出來了。

我們已經說過了DSL的好處, 這也就是Ant用XML而不直接用Java的原因。如果使用Lisp,我們可以任意創(chuàng)建DSL, 只要我們需要。我們可以創(chuàng)建用于網站程序的DSL, 可以寫多用戶游戲, 做固定收益貿易(fixed income trade), 解決蛋白質折疊問題, 處理事務問題, 等等。我們可以把這些疊放在一起, 造出一個語言, 專門解決基于網絡的貿易程序, 既有網絡語言的優(yōu)勢, 又有貿易語言的好處。每天我們都會收獲這種方法帶給我們的益處, 遠遠超過Ant所能給予我們的。

用DSL解決問題, 做出的程序精簡, 易于維護, 富有彈性。在Java里面, 我們可以用類來處理問題。這兩種方法的差別在于, Lisp使我們達到了一個更高層次的抽象, 我們不再受語言解析器本身的限制, 比較一下用Java庫直接寫的構造腳本和用Ant寫的構造腳本其間的差別。同樣的, 比較一下你以前所做的工作, 你就會明白Lisp帶來的好處。

“怎么理解Lisp的本質”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關的知識可以關注創(chuàng)新互聯網站,小編將為大家輸出更多高質量的實用文章!


分享標題:怎么理解Lisp的本質
網址分享:http://www.xueling.net.cn/article/ieepog.html

其他資訊

在線咨詢
服務熱線
服務熱線:028-86922220
TOP
主站蜘蛛池模板: 免费精品国产自产拍在线观看图片 | 国产激情精品视频 | 温柔少妇的高潮呻吟 | 欧美四级在线观看 | 欧美黄色激情视频 | 粉嫩av淫片一区二区三区 | A毛片终身免费观看网站 | 欧美日韩免费一区二区 | 国产国产成年年人免 | 91精品区 | 午夜影片 | 日本xxxx在线播放 | 亚洲欧美成人综合 | 狠狠色综合久久丁香婷婷 | 日日噜噜夜夜狠狠久久无码区 | 日本一区二区影院 | 国产亚洲欧美一区二区 | 国产精品美脚玉足脚交 | 日本一级在线观看 | 97久久超碰成人精品网站 | 国产农村熟妇出轨VIDEOS | 国产在人线免费视频精品 | 综合久久给合久久狠狠狠97色 | 日日爱666 | 日本爆乳j罩杯无码视频 | omofun动漫在线观看 | 特黄特色高清不卡免费视频 | 日本xxxx丰满老妇 | 啪啪精品视频 | 免费成年人视频在线观看 | 久久精品免费播放 | 无码AV综合AV亚洲AV | 999国内精品永久免费观看 | 亚洲熟妇AV日韩熟妇在线 | 福利视频一二三在线视频免费观看 | 成人无码H动漫网站免费 | 成人国产精品一区二区 | www.4hu影院 | 日韩在线一二三四区第一页 | 男人大臿蕉香蕉大视频 | 成人欧美一区二区三区在线播放 |