為什麼我喜歡 Lisp 程式語言

發表於2011-10-17

這篇文章是我在Simplificator——我工作的地方——的一次座談內容的摘錄,座談的題目叫做“為什麼我喜歡Smalltalk語言和Lisp語言”。在此之前,我曾釋出過一篇叫做“ 為什麼我喜歡Smalltalk?”的文章。

Lisp是一種很老的語言。非常的老。Lisp有很多變種,但如今已沒有一種語言叫Lisp的了。事實上,有多少Lisp程式設計師,就有多少種Lisp。這是因為,只有當你獨自一人深入荒漠,用樹枝在黃沙上為自己喜歡的Lisp方言寫直譯器時,你才成為一名真正的Lisp程式設計師

目前主要有兩種Lisp語言分支:Common LispScheme,每一種都有無數種的語言實現。各種Common Lisp實現都大同小異,而各種Scheme實現表現各異,有些看起來非常的不同,但它們的基本規則都相同。這兩種語言都非常有趣,但我卻沒有在實際工作中用過其中的任何一種。這兩種語言中分別在不同的方面讓我苦惱,在所有的Lisp方言中,我最喜歡的是Clojure語言。我不想在這個問題上做更多的討論,這是個人喜好,說起來很麻煩。

Clojure,就像其它種的Lisp語言一樣,有一個REPL(Read Eval Print Loop)環境,你可以在裡面寫程式碼,而且能馬上得到執行結果。例如:

通常,你會看到一個提示符,就像user>,但在本文中,我使用的是更實用的顯示風格。這篇文章中的任何REPL程式碼你都可以直接拷貝到Try Clojure執行。
我們可以像這樣呼叫一個函式:

程式列印出“Hello World”,並返回nil。我知道,這裡的括弧看起來好像放錯了地方,但這是有原因的,你會發現,他跟Java風格的程式碼沒有多少不同:

這種Clojure在執行任何操作時都要用到括弧:

在Clojure中,我們同樣能使用向量(vector):

還有符號(symbol):

這裡要用引號(‘),因為Symbol跟變數一樣,如果不用引號字首,Clojure會把它變成它的值。list資料型別也一樣:

以及巢狀的list:

定義變數和使用變數的方法像這樣:

我的講解會很快,很多細節問題都會忽略掉,有些我講的東西可能完全是錯誤的。請原諒,我盡力做到最好。

在Clojure中,建立函式的方法是這樣:

這顯示的又長又難看的東西是被編譯後的函式被列印出的樣子。不要擔心,你不會經常看到它們。這是個函式,使用fn操作符建立,有一個引數n。這個引數和2相乘,並當作結果返回。Clojure和其它所有的Lisp語言一樣,函式的最後一個表示式產生的值會被當作返回值返回。

如果你檢視一個函式如何被呼叫:

你會發現它的形式是,括弧,函式,引數,反括弧。或者用另一種方式描述,這是一個列表序列,序列的第一位是操作符,其餘的都是引數。

讓我們來呼叫這個函式:

我在這裡所做的是定義了一個匿名函式,並立即應用它。讓我們來給這個函式起個名字:

現在我們通過這個名字來使用它:

正像你看到的,函式就像其它資料一樣被存放到了變數裡。因為有些操作會反覆使用,我們可以使用簡化寫法:

我們使用if來給這個函式設定一個最大值:

if操作符有三個引數:斷言,當斷言是true時將要執行的語句,當斷言是 false 時將要執行的語句。也許寫成這樣更容易理解:

非常基礎的東西。讓我們來看一下更有趣的東西。
假設說你想把Lisp語句反著寫。把操作符放到最後,像這樣:

我們且把這種語言叫做Psil(反著寫的Lisp…我很聰明吧)。很顯然,如果你試圖執行這條語句,它會報錯:

Clojure會告訴你4不是一個函式(函式是必須是clojure.lang.IFn介面的實現)。

我們可以寫一個簡單的函式把Psil轉變成Lisp:

當我執行它時出現了問題:

很明顯,我弄錯了一個地方,因為在psil被呼叫之前,Clojure會先去執行它的引數,也就是(4 5 +),於是報錯了。我們可以顯式的把這個引數轉化成list,像這樣:

這回它就沒有被執行,但卻反轉了。要想執行它並不困難:

你開始發現Lisp的強大之處了。事實上,Lisp程式碼就是一堆層層巢狀的列表序列,你可以很容易從這些序列資料中產生可以執行的程式。

如果你還沒明白,你可以在你常用的語言中試一下。在陣列裡放入2個數和一個加號,通過陣列來執行這個運算。你最終得到的很可能是一個被連線的字串,或是其它怪異的結果。

這種程式設計方式在Lisp是如此的非常的常見,於是Lisp就提供了叫做巨集(macro)的可重用的東西來抽象出這種功能。巨集是一種函式,它接受未執行的引數,而返回的結果是可執行的Lisp程式碼。

讓我們把psil傳化成巨集:

唯一不同之處是我們現在使用defmacro來替換< code>defn。這是一個非常大的改動:

請注意,雖然引數並不是一個有效的Clojure引數,但程式並沒有報錯。這是因為引數並沒有被執行,只有當psil處理它時才被執行。psil把它的引數按資料看待。

如果你聽說過有人說Lisp裡程式碼就是資料,這就是我們現在在討論的東西了。資料可以被編輯,產生出其它的程式。這種特徵使你可以在Lisp語言上建立出任何你需要的新型語法語言。

在Clojure裡有一種操作符叫做macroexpand,它可以使一個巨集跳過可執行部分,這樣你就能看到是什麼樣的程式碼將會被執行:

你可以把巨集看作一個在編譯期執行的函式。事實上,在Lisp裡,編譯期和執行期是雜混在一起的,你的程式可以在這兩種狀態下來回切換。我們可以讓psil巨集變的羅嗦些,讓我們看看程式碼是如何執行的,但首先,我要先告訴你do這個東西。

do是一個很簡單的操作符,它接受一批語句,依次執行它們,但這些語句是被整體當作一個表示式,例如:

通過使用do,我們可以使巨集返回多個表示式,我們能看到更多的東西:

新巨集會列印出“compile time”,並且返回一個do程式碼塊,這個程式碼塊列印出“run time”,並且反著執行一個表示式。這個反引號`的作用很像引號',但它的獨特之處是你可以使用~符號在其內部解除引號。如果你聽不明白,不要擔心,讓我們來執行它一下:

如預期的結果,編譯期發生在執行期之前。如果我們使用macroexpand,或得到更清晰的資訊:

可以看出,編譯階段已經發生,得到的是一個將要列印出“run time”的語句,然後會執行(+ 5 4)println也被擴充套件成了它的完整形式,clojure.core/println,不過你可以忽略這個。然後程式碼在執行期被執行。

這個巨集的輸出本質上是:

而在巨集裡,它需要被寫成這樣:

反引號實際上是產生了一種模板形式的程式碼,而波浪號讓其中的某些部分被執行((reverse exp)),而其餘部分被保留。

對於巨集,其實還有更令人驚奇的東西,但現在,它已經很能變戲法了。

這種技術的力量還沒有被完全展現出來。按著” 為什麼我喜歡Smalltalk?”的思路,我們假設Clojure裡沒有if語法,只有cond語法。也許在這裡,這並不是一個太好的例子,但這個例子很簡單。

cond 功能跟其它語言裡的switchcase 很相似:

使用 cond,我們可以直接建立出my-if函式:

初看起來似乎好使:

但有一個問題。你能發現它嗎?my-if執行了它所有的引數,所以,如果我們像這樣做,它就不能產生預期的結果了:

my-if轉變成巨集:

問題解決了:

這只是對巨集的強大功能的窺豹一斑。一個非常有趣的案例是,當物件導向程式設計被發明出來後(Lisp的出現先於這概念),Lisp程式設計師想使用這種技術。

C程式設計師不得不使用他們的編譯器發明出新的語言,C++和Object C。Lisp程式設計師卻建立了一堆巨集,就像defclass, defmethod等。這全都要歸功於巨集。變革,在Lisp裡,只是一種進化。

相關文章