跨越邊界: Lisp 之美

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 的許多習語和特性有着很強的函數風格。從那以後,我學會了利用列表和函數式編程。

關於本系列

在 跨越邊界系列 文章中,作者 Bruce Tate 提出這樣一個觀點:如今的 Java 程序員可通過學習其他方法和語言得到很好的其他思路。自從 Java 技術明顯成爲所有開發項目的最佳選擇以來,編程前景已經變化。其他的框架影響着構建 Java 框架的方式,從其他語言學到的概念可以影響您的 Java 編程。您編寫的 Python(或 Ruby、Smalltalk……)代碼可以改變您處理 Java 編碼的方式。

本系列爲您介紹與 Java 開發根本不同,但也可以直接應用於 Java 開發的編程概念和技術。在一些示例中,需要對技術進行集成來利用它。在另外一些示例中,您將能夠直接應用這些概念。單獨的工具不及其他語言和框架能夠影響 Java 社區中的開發人員、框架甚至基本方法的思想那麼重要。

本期的跨越邊界 將重拾這份遺失的財富。我會帶您簡單地領略一下 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 中即爲列表。conslist 和 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 與 firstrest 一起用時可以構建任何列表。list 和 append 運算符只是爲了方便,但經常會用到它們。事實上,可以使用 consfirst 和 rest 來構建任何列表,或返回任何列表片段。例如,要獲取列表的第二或第三個元素,應該獲取 rest 中的 first,或 rest 中的 rest 中的 first,如清單 6 所示。或者,若要構建包含兩個或三個元素的列表,可以將 cons 和 firstrest 一起使用,來模擬 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 編程各方面的文章。 


獲得產品和技術

討論


關於作者

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,該公司專爲非營利組織和慈善機構謀求市場中的一席之地。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章