Lisp 長久以來一直被視為偉大的編程語言之一。其漫長的發展過程(接近五十年)中引發的追隨狂潮 表明:這是一門非同凡響的語言。在 MIT,Lisp 在所有程序員的課程中占了舉足輕重的地位。像 Paul Graham 那樣的企業家們將 Lisp 卓越的生產力用作他們事業成功起步的推動力。但令其追隨者懊惱萬分 的是,Lisp 從未成為主流編程語言。作為一名 Java™ 程序員,如果您花一點時間研究 Lisp 這座 被人遺忘的黃金之城,就會發現許多能夠改進編碼方式的技術。
我最近第一次完成了馬拉松賽跑 ,我發現跑步比我預想的更有價值。我跑了 26.2 英裡,通過該步驟,我開始認為這是對身體非常有益的 簡單活動。一些語言給了我類似的感覺,如 Smalltalk 和 Lisp。對 Smalltalk 來說,引發類似感覺的 是對象;Smalltalk 中的一切內容都是在處理對象和消息傳遞。對於 Lisp 來說,這個至為重要的步驟更 為簡單。這門語言完全由列表組成。但不要被這個簡單的假相所欺騙。這門有著 48 年歷史的語言具有難 以置信的強大功能和靈活性,這是 Java 語言所不能企及的。
第一次和 Lisp 打交道時,我還是 在校大學生,但這次不是很順利。因為我拼命地想把 Lisp 編入到熟悉的過程化范例中,而不是在 Lisp 的函數結構下工作。盡管 Lisp 並不是一門嚴格的函數語言(因為一些特性,它不符合最嚴格的術語定義 ),但 Lisp 的許多習語和特性有著很強的函數風格。從那以後,我學會了利用列表和函數式編程。
本期的跨越邊界 將重拾這份遺失的財富。我會帶您簡單地領略一下 Lisp 的基本構造,然後快速 的擴展開來。您將學到 Lambda 表達式、遞歸和宏。這份簡單的向導會讓您對 Lisp 的高效性和靈活性有 所理解。
入門
本文使用 GNU 的 GCL,它針對許多操作系統都有免費下載。但稍作修改, 就能使用任何版本的 Common Lisp。請參見 參考資料 獲取可用 Lisp 版本的詳細說明。
和學習大多數其他語言一樣,學習 Lisp 最好的方法就是實踐。打開您的解釋程序,和我一起編碼。 Lisp 基本上是一門編譯好的語言,通過直接鍵入命令,就可以輕松地用它進行編程。
列表語言
基本上,Lisp 是一門關於列表的語言。Lisp 中的一切內容(從數據到組成應用程序的代碼)都是列 表。每個列表都由一些原子 和列表組成。數字就是原子。鍵入一個數字僅僅會返回該數字作為結果:
清單 1. 簡單原子>1
1
>a
Error: The variable A is unbound.
如果鍵入一個字母,解釋程序會報錯,如清單 1 所示。字母是變量,所以使用之前必須先為其賦值。 如果想要引用一個字母或詞語而不是變量,請使用引號將其括起來。在變量前加單引號告訴 Lisp 延遲對 後續列表或原子進行求值,如清單 2 所示:
清單 2. 延遲求值和引用>"a"
"a"
>'a
A
請注意 Lisp 把 a 大寫為 A。lisp 假設您希望使用 A 作為符號,因為它沒有加括號。後面會討論賦 值,但先要讓列表來完成這一任務。簡單地講,Lisp 列表是加了括號並使用空格隔開的原子序列。嘗試 如清單 3 所示鍵入一個列表。這個列表是無效的,除非在列表前面加上 '。
清單 3. 鍵入一個簡單列表>(1 2 3)
Error: 1 is invalid as a function.
>'(1 2 3)
(1 2 3)
除非在列表前加上 ',否則 Lisp 會像對函數求值那樣對每個列表求值。第一個原子是運算符,列表 中其余的原子是參數。Lisp 有數目眾多的原語函數,正如您預料的那樣,其中包括許多數學函數,例如 ,+、* 和 sqrt。(+ 1 2 3) 返回 6,(* 1 2 3 4) 返回 12。
操縱列表的有兩類函數:構造函數 和選擇函數。構造函數構建列表,選擇函數分解列表。first 和 rest 是核心選擇函數。first 選擇函數返回列表的第一個原子,rest 選擇函數返回除第一個原子外的整 個列表。清單 4 顯示了這兩個選擇函數:
清單 4. 基本 Lisp 函數> (first '(lions tigers bears))
LIONS
> (rest '(lions tigers bears))
(TIGERS BEARS)
這兩個選擇函數都獲取整個列表,返回列表的主要片斷。稍後,您將了解遞歸如何利用這些選擇函數 。
如果希望構建列表而不是將其分開,就需要構造函數。與在 Java 語言中一樣,構造函數構建新元素 :在 Java 語言中為對象,在 Lisp 中即為列表。cons、list 和 append 是構造函數示例。核心構造函 數 cons 帶有兩個參數:一個原子和一個列表。cons 將該原子作為第一個元素添加到該列表。如果對 nil 調用 cons,Lisp 將 nil 作為空列表對待,並構建一個含一個元素的列表。append 連接兩個列表。 list 包含一個由所有參數組成的列表。清單 5 顯示了這些構造函數的實際應用:
清單 5. 使用構造函數> (cons 'lions '(tigers bears))
(LIONS TIGERS BEARS)
> (list 'lions 'tigers 'bears)
(LIONS TIGERS BEARS)
> (append '(lions) '(tigers bears))
(LIONS TIGERS BEARS)
將 cons 與 first、rest 一起用時可以構建任何列表。list 和 append 運算符只是為了方便,但經 常會用到它們。事實上,可以使用 cons、first 和 rest 來構建任何列表,或返回任何列表片段。例如 ,要獲取列表的第二或第三個元素,應該獲取 rest 中的 first,或 rest 中的 rest 中的 first,如清 單 6 所示。或者,若要構建包含兩個或三個元素的列表,可以將 cons 和 first、rest 一起使用,來模 擬 list 和 append。
清單 6. 構建第二個元素、第三個元素,然後模擬 list 和 append
>(first (rest '(1 2 3)))
2
>(first (rest (rest '(1 2 3))))
3
>(cons '1 (cons '2 nil))
(1 2)
>(cons '1 (cons '2 (cons '3 nil)))
(1 2 3)
>(cons (first '(1)) '(2 3))
(1 2 3)
這些示例也許無法引起您的興趣,但在如此簡單的原語之上構建一門簡潔優美的語言,其中的原理讓 一些程序員激動不已。這些由列表構建的簡單指令構成了遞歸、高階函數,甚至是閉包和 continuation 之類高級抽象的基礎。因此下面將研究高級抽象。
構建函數
可以猜到,Lisp 函數聲明為列表。清單 7 構建了一個返回列表第二個元素的函數,展示了函數聲明 的形式:
清單 7. 構建第二個函數(defun my_second (lst)
(first (rest lst))
)
defun 是用於定義自定義函數的函數。第一個參數是函數名,第二個參數是參數列表,第三個參數是 希望執行的代碼。可以看出,所有 Lisp 代碼都表述為列表。借助這項靈活和強大的功能,就可以像操縱 其他任何數據一樣操縱應用程序。稍後將看到一些示例使代碼和數據之間的區別變得模糊。
Lisp 也處理條件結構,如 if 語句。格式為 (if condition_statement then_statement else_statement)。清單 8 是一個簡單的 my_max 函數,用於計算兩個輸入變量中的最大值:
清單 8. 計算兩個整數中的最大值(defun my_max (x y)
(if (> x y) x y)
)
MY_MAX
(my_max 2 5)
5
(my_max 6 1)
6
下面回顧一下到目前為止看到的內容:
Lisp 使用列表和原子來表示數據和程序。
對列表求值時將第一個元素看作列表函數,將其他元素看作函數參數。
Lisp 條件語句將 true/false 表達式和代碼一起使用。
遞歸
Lisp 提供用於迭代的編碼結構,但遞歸是更受歡迎的列表遍歷方式。使用 first 和 rest 組合實現 遞歸效果很好。清單 9 中的 total 函數顯示了其運行原理:
清單 9. 使用遞歸計算列表的總和>(defun total (x)
(if (null x)
0
(+ (first x) (total (rest x)))
)
)
TOTAL
>(total '(1 5 1))
7
清單 9 中的 total 函數將列表當作單個的參數。第一個 if 語句在列表為空的情況下中斷遞歸,返 回零值。否則,該函數將第一個元素添加到列表其余部分的總和。現在應該明白如此構建 first 和 rest 的原因。first 能夠去除列表的第一個元素,rest 簡化了將尾部遞歸 (清單 9 中的遞歸類型)應用於 列表其余部分的過程。
由於性能的原因,Java 語言中的遞歸是有限的。Lisp 提供一項稱作尾部遞歸優化 的性能優化技術。 Lisp 編譯器或解釋器能夠將特定形式的遞歸翻譯為迭代,從而允許以一種更為簡單明快的方式來使用遞 歸數據結構(如樹結構)。
高階函數
如果模糊了數據和代碼之間的區別,Lisp 會更有意思。在本系列的前兩篇文章中,介紹了 JavaScript 中的高階函數 和 Ruby 中的閉包。這兩項功能都將函數作為參數進行傳遞。在 Lisp 中,由 於函數和列表沒有任何區別,高階函數也就非常簡單。
高階函數的最常見用法或許是 lambda 表達式,這是閉包的 Lisp 版。lambda 函數是用於將高階函數 傳入 Lisp 函數的函數定義。例如,清單 10 中的 lambda 表達式計算了兩個整數的和:
清單 10. Lambda 表達式>(setf total '(lambda (a b) (+ a b)))
(LAMBDA (A B) (+ A B))
>total
(LAMBDA (A B) (+ A B))
>(apply total '(101 102))
203
如果使用過高階函數或閉包,那麼可能更容易理解清單 10 中的代碼。第一行代碼定義了一個 lambda 表達式並將其和 total 符號綁定到一起。第二行代碼僅顯示了這個和 total 綁定到一起的 lambda 表達 式。最終,最後一個表達式對包含 (101 102) 的列表應用這個 lambda 表達式。
高階函數提供比面向對象概念更高層次的抽象。可以用它們來更簡潔清晰地表達想法。編程的至高境 界就是在不犧牲可讀性或性能的前提下,用更少的代碼提供更強大更靈活的功能。高階函數能實現所有這 些要求。
Lisp 還有兩種類型的高階函數。其中功能最強大的可能是宏。宏為後面的執行定義 Lisp 對象。可以 將宏看作代碼模板。請參考清單 11 中的示例:
清單 11. 宏>(defmacro times_two (x) (* 2 x))
TIMES_TWO
>(setf a 4)
4
>(times_two a)
8
這個示例應該分為兩個階段進行閱讀。第一次賦值定義了宏 times_two。在第二個階段(稱為宏擴展 )中,在對 a 求值之前,將 a 擴展為 (* 2 a)。該模板中這項延遲求值方式使宏的功能非常強大。Lisp 語言本身的許多功能都是基於宏的。
結束語
從年份上講,Lisp 也許很陳舊,甚至語法也很陳舊。但如果稍作研究,就會發現該語言有著難以置信 的強大功能,它的高階抽象一如既往地有效,並且生產力很高。許多更為現代的語言從 Lisp 中得到借鑒 ,但是其中大多數語言的功能無法與 Lisp 媲美。如果 Lisp 擁有 Java 或 .NET 的一部分市場,並且大 學中具備 lisp 知識的人也占有一定的比例,我們可能就會立即用它進行編碼。