Lisp的本質(The Nature of Lisp)

 Lisp的本質(The Nature of Lisp)

                             做者 Slava Akhmechet
                             譯者 Alec Jang

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


簡介

最初在web的某些角落偶然看到有人讚美Lisp時, 我那時已是一個很有經驗的程序員。
在個人履歷上, 掌握的語言範圍至關普遍, 象C++, Java, C#主流語言等等都不在話下, 
我以爲我差很少知道全部的有關編程語言的事情。對待編程語言的問題上, 我以爲本身不
太會遇到什麼大問題。其實我大錯特錯了。

我試着學了一下Lisp, 結果立刻就撞了牆。我被那些範例代碼嚇壞了。我想不少初次接觸
Lisp語言的人, 必定也有過相似的感覺。Lisp的語法太次了。一個語言的發明人, 竟然不
肯用心弄出一套漂亮的語法, 那誰還會願意學它。反正, 我是確確實實被那些難看的無數
的括號搞蒙了。

回過神來以後, 我和Lisp社區的那夥人交談, 訴說個人沮喪心情。結果, 立馬就有一大套
理論砸過來, 這套理論在Lisp社區到處可見, 幾成慣例。好比說: Lisp的括號只是表面現
象; Lisp的代碼和數據的表達方式沒有差異, 並且比XML語法高明許多, 因此有無窮的好
處; Lisp有強大無比的元語言能力, 程序員能夠寫出自我維護的代碼; Lisp能夠創造出針
對特定應用的語言子集; Lisp的運行時和編譯時沒有明確的分界; 等等, 等等, 等等。這
麼長的讚美詞雖然看起來至關動人, 不過對我毫無心義。沒人能給我演示這些東西是如何
應用的, 由於這些東西通常來講只有在大型系統纔會用到。我爭辯說, 這些東西傳統語言
同樣辦獲得。在和別人爭論了數個小時以後, 我最終仍是放棄了學Lisp的念頭。爲何要
花費幾個月的時間學習語法這麼難看的語言呢? 這種語言的概念這麼晦澀, 又沒什麼好懂
的例子。也許這語言不是該我這樣的人學的。

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

頓悟在忽然之間來臨。曾經許屢次, 我聽到別人引用雷蒙德(譯者注: 論文<<大教堂和市
集>>的做者, 著名的黑客社區理論家)的話: "Lisp語言值得學習。當你學會Lisp以後, 你
會擁有深入的體驗。就算你日常並不用Lisp編程, 它也會使你成爲更加優秀的程序員"。
過去, 我根本不懂這些話的含義, 我也不相信這是真的。但是如今我懂得了。這些話蘊含
的真理遠遠超過我過去的想像。我心裏體會到一種神聖的情感, 一瞬間的頓悟, 幾乎使我
對電腦科學的觀念發生了根本的改變。

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

我深刻地思考了這個問題。是否是Lisp有什麼很艱深的東西, 令得那麼多老練的程序員都
不能領會? 不是, 沒有任何絕對艱深的東西。由於我能弄懂, 我相信其餘人也必定能。那
麼問題出在那裏? 後來我終於找到了答案。個人結論就是, 凡是教人學高級概念, 必定要
從他已經懂得的東西開始。若是學習過程頗有趣, 學習的內容表達得很恰當, 新概念就會
變得至關直觀。這就是個人答案。所謂元編程, 所謂數據和代碼形式合一, 所謂自修改代
碼, 所謂特定應用的子語言, 全部這些概念根本就是同族概念, 彼此互爲解釋, 確定越講
越不明白。仍是從實際的例子出發最有用。

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

從新審視XML

千里之行始於足下。讓咱們的第一步從XML開始。但是XML已經說得更多的了, 還能有什麼
新意思可說呢? 有的。XML自身雖然談談不上有趣, 可是XML和Lisp的關係卻至關有趣。
XML和Lisp的概念有着驚人的類似之處。XML是咱們通向理解Lisp的橋樑。好吧, 咱們且把
XML看成活馬醫。讓咱們拿好手杖, 對XML的無人涉及的荒原地帶做一番探險。咱們要從一
個全新的視角來考察這個題目。

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

<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>

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

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

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

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

能把這個函數變成對等的XML格式嗎? 固然能夠。咱們能夠用不少種方式作到, 下面是其
中的一種, 十分簡單:

<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>

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

這一切充分說明, 咱們能夠把XML做爲源代碼的通用存儲方式, 其實咱們可以產生一整套
使用統一語法的程序語言, 也能寫出轉換器, 把已有代碼轉換成XML格式。若是真的採納
這種辦法, 各類語言的編譯器就用不着本身寫語法解析了, 它們能夠直接用XML的語法解
析來直接生成抽象語法樹。

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

咱們說到那裏了? 不錯, 咱們已經發現了一個有趣的關鍵之點。過去被認爲很難解的概念
已經很是直觀很是簡單的顯現出來。代碼也是數據, 而且歷來都是如此。這聽起來瘋瘋癲
癲的, 實際上倒是必然之事。我許諾過會以一種全新的方式來解釋Lisp, 我要重申個人許
諾。可是咱們此刻尚未到預約的地方, 因此仍是先繼續上邊的討論。

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

從新審視Ant

咱們如今已經來到了月亮背光的那一面, 先別忙着離開。再探索一下, 看看咱們還能發現
什麼東西。閉上眼睛, 想想2000年冬天的那個雨夜, 一個名叫James Duncan Davidson 
的傑出的程序員正在研究Tomcat的servlet容器。那時, 他正當心地保存好剛修改過的文
件, 而後執行make。結果冒出了一大堆錯誤, 顯然有什麼東西搞錯了。通過仔細檢查, 他
想, 難道是由於tab前面加了個空格而致使命令不能執行嗎? 確實如此。總是這樣, 他真
的受夠了。烏雲背後的月亮給了他啓示, 他建立了一個新的Java項目, 而後寫了一個簡單
可是十分有用的工具, 這個工具巧妙地利用了Java屬性文件中的信息來構造工程, 如今
James能夠寫makefile的替代品, 它能起到相同的做用, 而形式更加優美, 也不用擔憂有
makefile那樣可恨的空格問題。這個工具可以自動解釋屬性文件, 而後採起正確的動做來
編譯工程。真是簡單而優美。

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

使用Ant構造Tomcat以後幾個月, 他愈來愈感到Java的屬性文件不足以表達複雜的構造指
令。文件須要檢出, 拷貝, 編譯, 發到另一臺機器, 進行單元測試。要是出錯, 就發郵
件給相關人員, 要是成功, 就繼續在儘量高層的卷(volumn)上執行構造。追蹤到最後, 
卷要回復到最初的水平上。確實, Java的屬性文件不夠用了, James須要更有彈性的解決
方案。他不想本身寫解析器(由於他更但願有一個具備工業標準的方案)。XML看起來是個
不錯的選擇。他花了幾天工夫把Ant移植到XML,因而,一件偉大的工具誕生了。

Ant是怎樣工做的?原理很是簡單。Ant把包含有構造命令的XML文件(算代碼仍是算數據, 
你本身想吧),交給一個Java程序來解析每個元素,實際狀況比我說的還要簡單得多。
一個簡單的XML指令會致使具備相同名字的Java類裝入,並執行其代碼。

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

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

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

爲何是XML

有時候正確的決策並不是徹底出於深思熟慮。我不知道James選擇XML是否出於深思熟慮。也
許僅僅是個下意識的決定。至少從James在Ant網站上發表的文章看起來, 他所說的理由完
全是似是而非。他的主要理由是移植性和擴展性, 在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循環或者Java5中的foreach循環。若是咱們有一個轉換器, 能夠把XML轉換到Java, 大
概就會獲得上面這段事實上不能夠執行的代碼。由於Java的技術規範是定死的, 咱們沒有
辦法在程序裏改變它。咱們能夠增長包, 增長類, 增長方法, 可是咱們沒辦法增長算符, 
而對於XML, 咱們顯然能夠任由本身增長這樣的東西。對於XML的語法樹來講, 只要原意, 
咱們能夠任意增長任何元素, 所以等於咱們能夠任意增長算符。若是你還不太明白的話, 
看下面這個例子, 加入咱們要給Java引入一個unless算符:

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

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

對於複雜的算符來講, 這樣作的好處顯而易見。好比, 用特定的算符來作檢出源碼, 編譯
文件, 單元測試, 發送郵件等任務, 想一想看有多麼美妙。對於特定的題目, 好比說構造軟
件項目, 這些算符的使用能夠大幅減低少代碼的數量。增長代碼的清晰程度和可重用性。
解釋性的XML能夠很容易的達到這個目標。XML是存儲層次化數據的簡單數據文件, 而在
Java中, 因爲層次結構是定死的(你很快就會看到, Lisp的狀況與此大相徑庭), 咱們就沒
法達到上述目標。也許這正是Ant的成功之處呢。

你能夠注意一下最近Java和C#的變化(尤爲是C#3.0的技術規範), C#把經常使用的功能抽象出
來, 做爲算符增長到C#中。C#新增長的query算符就是一個例子。它用的仍是傳統的做法:
C#的設計者修改抽象語法樹, 而後增長對應的實現。若是程序員本身也能修改抽象語法樹
該有多好! 那樣咱們就能夠構造用於特定問題的子語言(好比說就像Ant這種用於構造項目
的語言), 你能想到別的例子嗎? 再思考一下這個概念。不過也沒必要思考太甚, 咱們待會
還會回到這個題目。那時候就會更加清晰。

離Lisp愈來愈近

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

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

若是XML支持"task"的建立, 上面這段代碼就會輸出"Hello World!". 實際上, 咱們能夠
用Java寫個"task"任務, 而後用Ant-XML來擴展它。Ant能夠在簡單原語的基礎上寫出更復
雜的原語, 就像其餘編程語言經常使用的做法同樣。這也就是咱們一開始提到的基於XML的編
程語言。這樣作用處不大(你知道爲甚麼嗎?), 可是真的很酷。

再看一回咱們剛纔說的Task任務。祝賀你呀, 你在看Lisp代碼!!! 我說什麼? 一點都不像
Lisp嗎? 不要緊, 咱們再給它收拾一下。

比XML更好

前面一節說過, Ant自我擴展沒什麼大用, 緣由在於XML很煩瑣。對於數據來講, 這個問題
還不太大, 但若是代碼很煩瑣的話, 光是打字上的麻煩就足以抵消它的好處。你寫過Ant 
的腳本嗎? 我寫過, 當腳本達到必定複雜度的時候, XML很是讓人厭煩。想一想看吧, 爲了
寫結束標籤, 每一個詞都得打兩遍, 不發瘋算好的!

爲了解決這個問題, 咱們應當簡化寫法。須知, XML僅僅是一種表達層次化數據的方式。
咱們並非必定要使用尖括號才能獲得樹的序列化結果。咱們徹底能夠採用其餘的格式。
其中的一種(恰好就是Lisp所採用的)格式, 叫作s表達式。s表達式要作的和XML同樣, 但
它的好處是寫法更簡單, 簡單的寫法更適合代碼輸入。後面我會詳細講s表達式。這以前
我要清理一下XML的東西。考慮一下關於拷貝文件的例子:

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

想一想看在內存裏面, 這段代碼的解析樹在內存會是什麼樣子? 會有一個"copy"節點, 其下
有一個 "fileset"節點, 可是屬性在哪裏呢? 它怎樣表達呢? 若是你之前用過XML, 而且
弄不清楚該用元素仍是該用屬性, 你不用感到孤單, 別人同樣糊塗着呢。沒人真的搞得清
楚。這個選擇與其說是基於技術的理由, 還不如說是閉着眼瞎摸。從概念上來說, 屬性也
是一種元素, 任何屬性能作的, 元素同樣作獲得。XML引入屬性的理由, 其實就是爲了讓
XML寫法不那麼冗長。好比咱們看個例子:

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

兩下比較, 內容的信息量徹底同樣, 用屬性能夠減小打字數量。若是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節點沒有什麼不一樣。

經受這一番折磨之後, 如今咱們終於至關接近Lisp了, Lisp的括號的神祕本質(就像許多
Lisp狂熱分子認爲的)逐漸顯現出來。如今咱們繼續研究其餘內容。

從新審視C語言的宏

到了這裏, 對XML的討論你大概都聽累了, 我都講累了。咱們先停一停, 把樹, s表達式,
Ant這些東西先放一放, 咱們來講說C的預處理器。必定有人問了, 咱們的話題和C有什麼
關係? 咱們已經知道了不少關於元編程的事情, 也探討過專門寫代碼的代碼。理解這問題
有必定難度, 由於相關討論文章所使用的編程語言, 都是大家不熟悉的。可是若是隻論概
唸的話, 就相對要簡單一些。我相信, 若是以C語言作例子來討論元編程, 理解起來必定
會容易得多。好, 咱們接着看。

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

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

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

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

元編程原本能夠用到無數多的地方, 但實際上使用的次數卻沒有那麼多。歸根結底, 咱們
內心仍是在盤算, 假設重複代碼用拷貝粘貼的話, 大概要重複6,7次, 對於這樣的工做量,
值得專門創建一套生成工具嗎? 固然不值得。數據訪問層和COM存根每每須要重用數百次,
甚至上千次, 因此用工具生成是最好的辦法。而那些僅僅是重複幾回十幾回的代碼, 是沒
有必要專門作工具的。沒必要要的時候也去開發代碼生成工具, 那就顯然過分估計了代碼生
成的好處。固然, 若是建立這類工具足夠簡單的話, 仍是應當儘可能多用, 由於這樣作必然
會節省時間。如今來看一下有沒有合理的辦法來達到這個目的。

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

    #define triple(X) X+X+X

這一行的做用是什麼? 這是一個簡單的預編譯指令, 它把程序中的triple(X)替換稱爲
X+X+X。例如, 把全部的triple(5)都換成5+5+5, 而後再交給編譯器編譯。這就是一個簡
單的代碼生成的例子。要是C的預處理器再強大一點, 要是可以容許鏈接數據庫, 要是能
多一些其餘簡單的機制, 咱們就能夠在咱們程序的內部開發本身的數據訪問層。下面這個
例子, 是一個假想的對C宏的擴展:

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

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

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

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

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

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

你好, Lisp

到此刻爲止, 咱們所知的關於Lisp的指示能夠總結爲一句話: Lisp是一個可執行的語法更
優美的XML, 但咱們尚未說Lisp是怎樣作到這一點的, 如今開始補上這個話題。 

Lisp有豐富的內置數據類型, 其中的整數和字符串和其餘語言沒什麼分別。像71或者
"hello"這樣的值, 含義也和C++或者Java這樣的語言大致相同。真正有意思的三種類型是
符號(symbol), 表和函數。這一章的剩餘部分, 我都會用來介紹這幾種類型, 還要介紹
Lisp環境是怎樣編譯和運行源碼的。這個過程用Lisp的術語來講一般叫作求值。通讀這一
節內容, 對於透徹理解元編程的真正潛力, 以及代碼和數據的同一性, 和麪向領域語言的
觀念, 都極其重要。萬勿等閒視之。我會盡可能講得生動有趣一些, 也但願你能得到一些
啓發。那好, 咱們先講符號。

大致上, 符號至關於C++或Java語言中的標誌符, 它的名字能夠用來訪問變量值(例如
currentTime, arrayCount, n, 等等), 差異在於, Lisp中的符號更加基本。在C++或
Java裏面, 變量名只能用字母和下劃線的組合, 而Lisp的符號則很是有包容性, 好比, 加
號(+)就是一個合法的符號, 其餘的像-, =, hello-world, *等等均可以是符號名。符號
名的命名規則能夠在網上查到。你能夠給這些符號任意賦值, 咱們這裏先用僞碼來講明這
一點。假定函數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語言的情形很不相同。
在這裏函數本身表明本身。事實上它是一個指向代碼塊的指針, 附帶有一些其餘信息(例
如一組參數變量)。只有在把函數賦予其餘符號時, 它才具備了名字, 就像把一個數值或
字符串賦予變量同樣的道理。你能夠用一個內置的專門用於建立函數的函數來建立函數,
而後把它賦值給符號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系統遇到這樣的表時, 它所作的, 和Ant處理XML數據所作的, 很是類似, 那就是試
圖執行它們。其實, Lisp源碼就是特定的一種表, 比如Ant源碼是一種特定的XML同樣。
Lisp執行表的順序是這樣的, 表的第一個元素看成函數, 其餘元素看成函數的參數。若是
其中某個參數也是表, 那就按照一樣的原則對這個表求值, 結果再傳遞給最初的函數做爲
參數。這就是基本原則。咱們看一下真正的代碼:

    (* 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節點, 這樣的話內存中
的節點就能夠自動序列化成爲字符串。不只如此, 咱們的工具還能夠分析這些節點, 對已
有的XML文件作變換。經過直接處理XML節點。咱們能夠超越字符串處理, 使用更高層次的
概念, 所以咱們的工做就會作的更快更好。

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

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

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

    <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>

相應的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)做爲一個表傳遞給宏, 而後宏就能夠任意處理這個表, 固然也能夠對它求
值。宏的返回值是一個表, 而後有程序做爲代碼來執行。宏所佔的位置, 就被替換爲這個
結果代碼。咱們能夠定義一個宏把數據替換爲任意代碼, 比方說, 替換爲顯示數據給用戶
的代碼。

這和元編程, 以及咱們要作的任務表程序有什麼關係呢? 實際上, 編譯器會替咱們工做, 
調用相應的宏。咱們所要作的, 僅僅是建立一個把數據轉換爲適當代碼的宏。

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

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

(譯註: 在Common Lisp中, 此處的單引號應當是反單引號, 意思是對錶不求值, 但能夠對
表中某元素求值, 記號~表示對元素x求值, 這個求值記號在Common Lisp中應當是逗號。
反單引號和單引號的區別是, 單引號標識的表, 其中的元素都不求值。這裏做者所用的記
號是本身發明的一種Lisp方言Blaise, 和common lisp略有不一樣, 事實上, 發明方言是
lisp高手獨有的樂趣, 不少狂熱分子都熱衷這樣作。好比Paul Graham就發明了ARC, 許多
記號比傳統的Lisp簡潔得多, 顯得比較現代)

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

    (triple 4)

都會被替換成:

    (+ 4 4 4)

咱們能夠爲任務表程序寫一個宏, 把任務數據轉換爲可執行碼, 而後執行。假定咱們的輸
出是在控制檯:

    (defmacro item (priority note)
        `(block 
            (print stdout tab "Prority: " ~(head (tail priority)) endl)
            (print stdout tab "Note: " ~note endl endl)))

咱們創造了一個很是小的有限的語言來管理嵌在Lisp中的任務表。這個語言只用來解決特
定領域的問題, 一般稱之爲DSLs(特定領域語言, 或專用領域語言)。

特定領域語言

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

咱們已經說過了DSL的好處, 這也就是Ant用XML而不直接用Java的緣由。若是使用Lisp, 
咱們能夠任意建立DSL, 只要咱們須要。咱們能夠建立用於網站程序的DSL, 能夠寫多用戶
遊戲, 作固定收益貿易(fixed income trade), 解決蛋白質摺疊問題, 處理事務問題, 等
等。咱們能夠把這些疊放在一塊兒, 造出一個語言, 專門解決基於網絡的貿易程序, 既有網
絡語言的優點, 又有貿易語言的好處。天天咱們都會收穫這種方法帶給咱們的益處, 遠遠
超過Ant所能給予咱們的。

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

接下來

學習Lisp就像戰爭中爭奪山頭。儘管在電腦科學領域, Lisp已經算是一門古老的語言, 直
到如今仍然不多有人真的明白該怎樣給初學者講授Lisp。儘管Lisp老手們盡了很大努力,
今天新手學習Lisp仍然是困難重重。好在如今事情正在發生變化, Lisp的資源正在迅速增
加, 隨着時間推移, Lisp將會愈來愈受關注。

Lisp令人超越平庸, 走到前沿。學會Lisp意味着你能找到更好的工做, 由於聰明的僱主會
被你不同凡響的洞察力所打動。學會Lisp也可能意味着明天你可能會被解僱, 由於你老是
強調, 若是公司全部軟件都用Lisp寫, 公司將會如何卓越, 而這些話你的同事會聽煩的。
Lisp值得努力學習嗎? 那些已經學會Lisp的人都說值得, 固然, 這取決於你的判斷。

你的見解呢?

這篇文章寫寫停停, 用了幾個月才最終完成。若是你以爲有趣, 或者有什麼問題, 意見或
建議, 請給我發郵件coffeemug@gmail.com, 我會很高興收到你的反饋。html

相關文章
相關標籤/搜索