跨越邊界: Lisp 之美

Lisp 長久以來一直被視爲偉大的編程語言之一。其漫長的發展過程(接近五十年)中引起的追隨狂潮代表:這是一門非同凡響的語言。在 MIT,Lisp 在全部程序員的課程中佔了舉足輕重的地位。像 Paul Graham 那樣的企業家們將 Lisp 卓越的生產力用做他們事業成功起步的推進力。但令其追隨者懊惱萬分的是,Lisp 從未成爲主流編程語言。做爲一名 Java? 程序員,若是您花一點時間研究 Lisp 這座被人遺忘的黃金之城,就會發現許多可以改進編碼方式的技術。

我最近第一次完成了馬拉松賽跑,我 發現跑步比我預想的更有價值。我跑了 26.2 英里,經過該步驟,我開始認爲這是對身體很是有益的簡單活動。一些語言給了我相似的感受,如 Smalltalk 和 Lisp。對 Smalltalk 來講,引起相似感受的是對象;Smalltalk 中的一切內容都是在處理對象和消息傳遞。對於 Lisp 來講,這個至爲重要的步驟更爲簡單。這門語言徹底由列表組成。但不要被這個簡單的假相所欺騙。這門有着 48 年曆史的語言具備難以置信的強大功能和靈活性,這是 Java 語言所不能企及的。 html

第一次和 Lisp 打交道時,我仍是在校大學生,但此次不是很順利。由於我拼命地想把 Lisp 編入到熟悉的過程化範例中,而不是在 Lisp 的函數結構下工做。儘管 Lisp 並非一門嚴格的函數語言(由於一些特性,它不符合最嚴格的術語定義),但 Lisp 的許多習語和特性有着很強的函數風格。從那之後,我學會了利用列表和函數式編程。 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 所示: app


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

操縱列表的有兩類函數:構造函數選擇函數。構造函數構建列表,選擇函數分解列表。firstrest 是核心選擇函數。first 選擇函數返回列表的第一個原子,rest 選擇函數返回除第一個原子外的整個列表。清單 4 顯示了這兩個選擇函數:


清單 4. 基本 Lisp 函數

> (first '(lions tigers bears))
LIONS

> (rest '(lions tigers bears))
(TIGERS BEARS)

這兩個選擇函數都獲取整個列表,返回列表的主要片段。稍後,您將瞭解遞歸如何利用這些選擇函數。

若是但願構建列表而不是將其分開,就須要構造函數。與在 Java 語言中同樣,構造函數構建新元素:在 Java 語言中爲對象,在 Lisp 中即爲列表。conslistappend 是構造函數示例。核心構造函數 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)

consfirstrest 一塊兒用時能夠構建任何列表。listappend 運算符只是爲了方便,但常常會用到它們。事實上,可使用 consfirstrest 來構建任何列表,或返回任何列表片斷。例如,要獲取列表的第二或第三個元素,應該獲取 rest 中的 first,或 rest 中的 rest 中的 first,如清單 6 所示。或者,若要構建包含兩個或三個元素的列表,能夠將 consfirstrest 一塊兒使用,來模擬 listappend


清單 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 提供用於迭代的編碼結構,但遞歸是更受歡迎的列表遍歷方式。使用 firstrest 組合實現遞歸效果很好。清單 9 中的 total 函數顯示了其運行原理:


清單 9. 使用遞歸計算列表的總和

>(defun total (x)
(if (null x)
0
(+ (first x) (total (rest x)))
)
)

TOTAL
>(total '(1 5 1))

7

清單 9 中的 total 函數將列表看成單個的參數。第一個 if 語句在列表爲空的狀況下中斷遞歸,返回零值。不然,該函數將第一個元素添加到列表其他部分的總和。如今應該明白如此構建 firstrest 的緣由。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 編程各方面的文章。


得到產品和技術

討論
相關文章
相關標籤/搜索