Bruce Tate ([email protected]), CTO, WellGood LLC
2007 年 3 月 05 日
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)
返回 24
。
操縱列表的有兩類函數:構造函數 和選擇函數。構造函數構建列表,選擇函數分解列表。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 知識的人也佔有一定的比例,我們可能就會立即用它進行編碼。
學習
- 您可以參閱本文在 developerWorks 全球站點上的 英文原文 。
- Beyond Java (O'Reilly,2005 年):本文作者編寫的一本書,講述 Java 語言優缺點以及在某些方面可能對 Java 平臺帶來挑戰的技術。
- GNU Common Lisp:一個更爲流行的 Lisp 實現,也是本文中使用的 Lisp 解釋器。
- Carl de Marcken: Inside Orbitz:這個關於 Lisp 實際功能的討論展示了 Lisp 在現實世界中能完成的工作。
- Learning Lisp:一本關於 Lisp 的優秀初級讀物,構成了本文中一些示例的基礎。
- Structure and Interpretation of Computer Programs,第 2 版(Harold Abelson et al.,McGraw-Hill,1996 年):一本以 Lisp 哲學爲基礎的經典讀物。
- Association of Lisp Users:支持 Lisp 社區的國際組織。
- Java 技術專區:這裏可以找到數百篇關於 Java 編程各方面的文章。
獲得產品和技術
- Common Lisp Implementations:商業和免費的 Common Lisp 實現。
討論
- 通過參與 developerWorks blog 加入 developerWorks 社區。
Bruce Tate 居住在德克薩斯州的奧斯汀,他是一位父親,同時也是山地車手和皮艇手。他是 3 本 Java 暢銷書籍的作者,其中包括榮獲 Jolt 大獎的 Better, Faster, Lighter Java 一書。他最近又出版了 From Java to Ruby 和 Rails: Up and Running。他在 IBM 工作了 13 年,隨後創建了 RapidRed 顧問公司,在那裏他潛心研究基於 Ruby 和 Ruby on Rails 框架的輕量級開發策略和架構。如今,他是 WellGood LLC 公司的 CTO,該公司專爲非營利組織和慈善機構謀求市場中的一席之地。 |