正體中文¶前言¶本書的目的是快速及全面的教你 Common Lisp 的有關知識。它實際上包含兩本書。前半部分用大量的例子來解釋 Common Lisp 裡面重要的概念。後半部分是一個最新 Common Lisp 辭典,涵蓋了所有 ANSI Common Lisp 的運算子。 這本書的目標讀者¶ANSI Common Lisp 這本書適合學生或者是專業的程式設計師去讀。本書假設讀者閱讀前沒有 Lisp 的相關知識。有別的程式語言的編程經驗也許對讀本書有幫助,但也不是必須的。本書從解釋 Lisp 中最基本的概念開始,並對於 Lisp 最容易迷惑初學者的地方進行特別的強調。 本書也可以作爲教授 Lisp 編程的課本,也可以作爲人工智能課程和其他編程語言課程中,有關 Lisp 部分的參考書。想要學習 Lisp 的專業程式設計師肯定會很喜歡本書所採用的直截了當、注重實踐的方法。那些已經在使用 Lisp 編程的人士將會在本書中發現許多有用的實體,此外,本書也是一本方便的 ANSI Common Lisp 參考書。 如何使用這本書¶學習 Lisp 最好的辦法就是拿它來編程。況且在學習的同時用你學到的技術進行編程,也是非常有趣的一件事。編寫本書的目的就是讓讀者儘快的入門,在對 Lisp 進行簡短的介紹之後, 第 2 章開始用 21 頁的內容,介紹了著手編寫 Lisp 程式時可能會用到的所有知識。 3-9 章講解了 Lisp 裡面一些重要的知識點。這些章節特彆強調了一些重要的概念,比如指標在 Lisp 中扮演的角色,如何使用遞迴來解決問題,以及第一級函數的重要性等等。 針對那些想要更深入了解 Lisp 的讀者: 10-14 章包含了宏、CLOS、列表操作、程式優化,以及一些更高級的課題,比如包和讀取宏。 15-17 章通過 3 個 Common Lisp 的實際應用,總結了之前章節所講解的知識:一個是進行邏輯推理的程式,另一個是 HTML 生成器,最後一個是針對物件導向程式設計的嵌入式語言。 本書的最後一部分包含了 4 個附錄,這些附錄應該對所有的讀者都有用: 附錄 A-D 包括了一個如何除錯程式的指南, 58 個 Common Lisp 運算子的源程式,一個關於 ANSI Common Lisp 和以前的 Lisp 語言區別的總結,以及一個包括所有 ANSI Common Lisp 的參考手冊。 本書還包括一節備註。這些備註包括一些說明,一些參考條目,一些額外的程式,以及一些對偶然出現的不正確表述的糾正。備註在文中用一個小圓圈來表示,像這樣:○ Tip 譯註: 由於小圈圈 ○ 實在太不明顯了,譯文中使用 λ 符號來表示備註。 原始碼¶雖然本書介紹的是 ANSI Common Lisp ,但是本書中的程式可以在任何版本的 Common Lisp 中運行。那些依賴 Lisp 語言新特性的例子的旁邊,會有註釋告訴你如何把它們運行於舊版本的 Lisp 中。 本書中所有的程式都可以在互聯網上下載到。你可以在網路上找到這些程式,它們還附帶著一個免費軟體的連結,一些過去的論文,以及 Lisp 的 FAQ 。還有很多有關 Lisp 的資源可以在此找到:http://www.eecs.harvard.edu/onlisp/ 原始碼可以在此 FTP 服務器上下載: ftp://ftp.eecs.harvard.edu:/pub/onlisp/ 讀者的問題和意見可以發送到 pg@eecs.harvard.edu 。 Note 譯註:下載的連結都壞掉了,本書的原始碼可以到此下載:https://raw.github.com/acl-translation/acl-chinese/master/code/acl2.lisp On Lisp¶在整本 On Lisp 書中,我一直試著指出一些 Lisp 獨一無二的特性,這些特性使得 Lisp 更像 “Lisp” 。並展示一些 Lisp 能讓你完成的新事情。比如說宏: Lisp 程式設計師能夠並且經常編寫一些能夠寫程式的程式。對於程式生成程式這種特性,因爲 Lisp 是主流語言中唯一一個提供了相關抽象使得你能夠方便地實現這種特性的編程語言,所以 Lisp 是主流語言中唯一一個廣泛運用這個特性的語言。我非常樂意邀請那些想要更進一步了解宏和其他高級 Lisp 技術的讀者,讀一下本書的姐妹篇:On Lisp。 Note On Lisp 已經由知名 Lisp 黑客 ── 田春 ── 翻譯完成,可以在網路上找到。 ── 田春(知名 Lisp 黑客、Practical Common Lisp 譯者) 鳴謝¶在所有幫助我完成這本的朋友當中,我想特別的感謝一下 Robert Morris 。他的重要影響反應在整本書中。他的良好影響使這本書更加優秀。本書中好一些實體程式都源自他手。這些程式包括 138 頁的 Henley 和 249 頁的模式匹配器。 我很高興能有一個高水平的技術審稿小組:Skona Brittain, John Foderaro, Nick Levine, Peter Norvig 和 Dave Touretzky。本書中幾乎所有部分都得益於它們的意見。 John Foderaro 甚至重寫了本書 5.7 節中一些程式。 另外一些人通篇閱讀了本書的手稿,它們是:Ken Anderson, Tom Cheatham, Richard Fateman, Steve Hain, Barry Margolin, Waldo Pacheco, Wheeler Ruml 和 Stuart Russell。特別要提一下,Ken Anderson 和 Wheeler Ruml 給予了很多有用的意見。 我非常感謝 Cheatham 教授,更廣泛的說,哈佛,提供我編寫這本書的一些必要條件。另外也要感謝 Aiken 實驗室的人員:Tony Hartman, Dave Mazieres, Janusz Juda, Harry Bochner 和 Joanne Klys。 我非常高興能再一次有機會和 Alan Apt 合作。還有這些在 Prentice Hall 工作的人士: Alan, Mona, Pompili Shirley McGuire 和 Shirley Michaels, 能與你們共事我很高興。 本書用 Leslie Lamport 寫的 LaTeX 進行排版。LaTeX 是在 Donald Knuth 編寫的 TeX 的基礎上,又加了 L.A.Carr, Van Jacobson 和 Guy Steele 所編寫的宏完成。書中的圖表是由 John Vlissides 和 Scott Stanton 編寫的 Idraw 完成的。整本書的預覽是由 Tim Theisen 寫的 Ghostview 完成的。 Ghostview 是根據 L. Peter Deutsch 的 Ghostscript 創建的。 我還需要感謝其他的許多人,包括:Henry Baker, Kim Barrett, Ingrid Bassett, Trevor Blackwell, Paul Becker, Gary Bisbee, Frank Deutschmann, Frances Dickey, Rich 和 Scott Draves, Bill Dubuque, Dan Friedman, Jenny Graham, Alice Hartley, David Hendler, Mike Hewett, Glenn Holloway, Brad Karp, Sonya Keene, Ross Knights, Mutsumi Komuro, Steffi Kutzia, David Kuznick, Madi Lord, Julie Mallozzi, Paul McNamee, Dave Moon, Howard Mullings, Mark Nitzberg, Nancy Parmet 和其家人, Robert Penny, Mike Plusch, Cheryl Sacks, Hazem Sayed, Shannon Spires, Lou Steinberg, Paul Stoddard, John Stone, Guy Steele, Steve Strassmann, Jim Veitch, Dave Watkins, Idelle and Julian Weber, the Weickers, Dave Yost 和 Alan Yuille。 另外,著重感謝我的父母和 Jackie。 高德納給他的經典叢書起名爲《計算機程式設計藝術》。在他的圖靈獎獲獎感言中,他解釋說這本書的書名源自於內心深處的潛意識 ── 潛意識告訴他,編程其實就是追求編寫最優美的程式。 就像建築設計一樣,編程既是一門工程技藝也是一門藝術。一個程式要遵循數學原理也要符合物理定律。但是建築師的目的不僅僅是建一個不會倒塌的建築。更重要的是,他們要建一個優美的建築。 像高德納一樣,很多程式設計師認爲編程的真正目的,不僅僅是編寫出正確的程式,更重要的是寫出優美的程式。幾乎所有的 Lisp 黑客也是這麼想的。 Lisp 黑客精神可以用兩句話來概括:編程應該是有趣的。程式應該是優美的。這就是我在這本書中想要傳達的精神。 第一章:簡介¶約翰麥卡錫和他的學生於 1958 年展開 Lisp 的初次實現工作。 Lisp 是繼 FORTRAN 之後,仍在使用的最古老的程式語言。 λ 更值得注意的是,它仍走在程式語言技術的最前面。懂 Lisp 的程式設計師會告訴你,有某種東西使 Lisp 與衆不同。 Lisp 與衆不同的部分原因是,它被設計成能夠自己進化。你能用 Lisp 定義新的 Lisp 運算子。當新的抽象概念風行時(如物件導向程式設計),我們總是發現這些新概念在 Lisp 是最容易來實現的。Lisp 就像生物的 DNA 一樣,這樣的語言永遠不會過時。 1.1 新的工具(New Tools)¶爲什麼要學 Lisp?因爲它讓你能做一些其它語言做不到的事情。如果你只想寫一個函數來返回小於 ; Lisp /* C */
(defun sum (n) int sum(int n){
(let ((s 0)) int i, s = 0;
(dotimes (i n s) for(i = 0; i < n; i++)
(incf s i)))) s += i;
return(s);
}
如果你只想做這種簡單的事情,那用什麼語言都不重要。假設你想寫一個函數,輸入一個數 ; Lisp
(defun addn (n)
#'(lambda (x)
(+ x n)))
在 C 語言中 你可能會想,誰會想做這樣的事情?程式語言教你不要做它們沒有提供的事情。你得針對每個程式語言,用其特定的思維來寫程式,而且想得到你所不能描述的東西是很困難的。當我剛開始編程時 ── 用 Baisc ── 我不知道什麼是遞迴,因爲我根本不知道有這個東西。我是用 Basic 在思考。我只能用迭代的概念表達算法,所以我怎麼會知道遞迴呢? 如果你沒聽過詞法閉包 「Lexical Closure」 (上述 閉包僅是其中一個我們在別的語言找不到的抽象概念之一。另一個更有價值的 Lisp 特點是, Lisp 程式是用 Lisp 的資料結構來表示。這表示你可以寫出會寫程式的程式。人們真的需要這個嗎?沒錯 ── 它們叫做宏,有經驗的程式設計師也一直在使用它。學到 173 頁你就可以自己寫出自己的宏了。 有了宏、閉包以及運行期型別,Lisp 凌駕在物件導向程式設計之上。如果你了解上面那句話,也許你不應該閱讀此書。你得充分了解 Lisp 才能明白爲什麼此言不虛。但這不是空泛之言。這是一個重要的論點,並且在 17 章用程式相當明確的證明了這點。 第二章到第十三章會循序漸進地介紹所有你需要理解第 17 章程式的概念。你的努力會有所回報:你會感到在 C++ 編程是窒礙難行的,就像有經驗的 C++ 程式設計師用 Basic 編程會感到窒息一樣。更加鼓舞人心的是,如果我們思考爲什麼會有這種感覺。 編寫 Basic 對於平常用 C++ 編程是令人感到窒息的,是因爲有經驗的 C++ 程式設計師知道一些用 Basic 不可能表達出來的技術。同樣地,學習 Lisp 不僅教你學會一門新的語言 ── 它教你嶄新的並且更強大的程式思考方法。 1.2 新的技術(New Techniques)¶如上一節所提到的, Lisp 賦予你別的語言所沒有的工具。不僅僅如此,就 Lisp 帶來的新特性來說 ── 自動記憶體管理 (automatic memory management),顯式型別 (manifest typing),閉包 (closures)等 ── 每一項都使得編程變得如此簡單。結合起來,它們組成了一個關鍵的部分,使得一種新的編程方式是有可能的。 Lisp 被設計成可擴展的:讓你定義自己的運算子。這是可能的,因爲 Lisp 是由和你程式一樣的函數與宏所構成的。所以擴展 Lisp 就和寫一個 Lisp 程式一樣簡單。事實上,它是如此的容易(和有用),以至於擴展語言自身成了標準實踐。當你在用 Lisp 語言編程時,你也在創造一個適合你的程式的語言。你由下而上地,也由上而下地工作。 幾乎所有的程式,都可以從訂作適合自己所需的語言中受益。然而越複雜的程式,由下而上的程式設計就顯得越有價值。一個由下而上所設計出來的程式,可寫成一系列的層,每層擔任上一層的程式語言。 TeX 是最早使用這種方法所寫的程式之一。你可以用任何語言由下而上地設計程式,但 Lisp 是本質上最適合這種方法的工具。 由下而上的編程方法,自然發展出可擴展的軟體。如果你把由下而上的程式設計的原則,想成你程式的最上層,那這層就成爲使用者的程式語言。正因可擴展的思想深植於 Lisp 當中,使得 Lisp 成爲實現可擴展軟體的理想語言。三個 1980 年代最成功的程式提供 Lisp 作爲擴展自身的語言: GNU Emacs , Autocad ,和 Interleaf 。 由下而上的編程方法,也是得到可重用軟體的最好方法。寫可重用軟體的本質是把共同的地方從細節中分離出來,而由下而上的編程方法本質地創造這種分離。與其努力撰寫一個龐大的應用,不如努力創造一個語言,用相對小的努力在這語言上撰寫你的應用。和應用相關的特性集中在最上層,以下的層可以組成一個適合這種應用的語言 ── 還有什麼比程式語言更具可重用性的呢? Lisp 讓你不僅編寫出更複雜的程式,而且寫的更快。 Lisp 程式通常很簡短 ── Lisp 給了你更高的抽象化,所以你不用寫太多程式碼。就像 Frederick Brooks 所指出的,編程所花的時間主要取決於程式的長度。因此僅僅根據這個單獨的事實,就可以推斷出用 Lisp 編程所花的時間較少。這種效果被 Lisp 的動態特點放大了:在 Lisp 中,編輯-編譯-測試迴圈短到使編程像是即時的。 更高的抽象化與互動的環境,能改變各個機構開發軟體的方式。術語快速建型描述了一種始於 Lisp 的編程方法:在 Lisp 裡,你可以用比寫規格說明更短的時間,寫一個原型出來,而這種原型是高度抽象化的,可作爲一個比用英語所寫的更好的規格說明。而且 Lisp 讓你可以輕易的從原型轉成產品軟體。當寫一個考慮到速度的 Common Lisp 程式時,通過現代編譯器的編譯,Lisp 與其他的高階語言所寫的程式運行得一樣快。 除非你相當熟悉 Lisp ,這個簡介像是無意義的言論和冠冕堂皇的宣告。Lisp 凌駕物件導向程式設計? 你創造適合你程式的語言? Lisp 編程是即時的? 這些說法是什麼意思?現在這些說法就像是枯竭的湖泊。隨著你學到更多實際的 Lisp 特色,見過更多可運行的程式,這些說法就會被實際經驗之水所充滿,而有了明確的形狀。 1.3 新的方法(New Approach)¶本書的目標之一是不僅是教授 Lisp 語言,而是教授一種新的編程方法,這種方法因爲有了 Lisp 而有可能實現。這是一種你在未來會見得更多的方法。隨著開發環境變得更強大,程式語言變得更抽象, Lisp 的編程風格正逐漸取代舊的規劃-然後-實現 (plan-and-implement)的模式。 在舊的模式中,錯誤永遠不應該出現。事前辛苦訂出縝密的規格說明,確保程式完美的運行。理論上聽起來不錯。不幸地,規格說明是人寫的,也是人來實現的。實際上結果是, 規劃-然後-實現 模型不太有效。 身爲 OS/360 的項目經理, Frederick Brooks 非常熟悉這種傳統的模式。他也非常熟悉它的後果:
而這卻描述了那個時代最成功系統之一。 舊模式的問題是它忽略了人的侷限性。在舊模式中,你打賭規格說明不會有嚴重的缺失,實現它們不過是把規格轉成程式碼的簡單事情。經驗顯示這實在是非常壞的賭注。打賭規格說明是誤導的,程式到處都是臭蟲 (bug) 會更保險一點。 這其實就是新的編程模式所假設的。設法儘量降低錯誤的成本,而不是希望人們不犯錯。錯誤的成本是修補它所花費的時間。使用強大的語言跟好的開發環境,這種成本會大幅地降低。編程風格可以更多地依靠探索,較少地依靠事前規劃。 規劃是一種必要之惡。它是評估風險的指標:越是危險,預先規劃就顯得更重要。強大的工具降低了風險,也降低了規劃的需求。程式的設計可以從最有用的資訊來源中受益:過去實作程式的經驗。 Lisp 風格從 1960 年代一直朝著這個方向演進。你在 Lisp 中可以如此快速地寫出原型,以致於你已歷經好幾個設計和實現的迴圈,而在舊的模式當中,你可能才剛寫完規格說明。你不必擔心設計的缺失,因爲你將更快地發現它們。你也不用擔心有那麼多臭蟲。當你用函數式風格來編程,你的臭蟲只有區域的影響。當你使用一種很抽象的語言,某些臭蟲(如迷途指標)不再可能發生,而剩下的臭蟲很容易找出,因爲你的程式更短了。當你有一個互動的開發環境,你可以即時修補臭蟲,不必經歷 編輯,編譯,測試的漫長過程。 Lisp 風格會這麼演進是因爲它產生的結果。聽起來很奇怪,少的規劃意味著更好的設計。技術史上相似的例子不勝列舉。一個相似的變革發生在十五世紀的繪畫圈裡。在油畫流行前,畫家使用一種叫做蛋彩的材料來作畫。蛋彩不能被混和或塗掉。犯錯的代價非常高,也使得畫家變得保守。後來隨著油畫顏料的出現,作畫風格有了大幅地改變。油畫“允許你再來一次”這對困難主題的處理,像是畫人體,提供了決定性的有利條件。 新的材料不僅使畫家更容易作畫了。它使新的更大膽的作畫方式成爲可能。 Janson 寫道:
做爲一種介質,蛋彩與油畫顏料一樣美麗。但油畫顏料的彈性給想像力更大的發揮空間 ── 這是決定性的因素。 程式設計正經歷著相同的改變。新的介質像是“動態的物件導向語言” ── 即 Lisp 。這不是說我們所有的軟體在幾年內都要用 Lisp 來寫。從蛋彩到油畫的轉變也不是一夜完成的;油彩一開始只在領先的藝術中心流行,而且經常混合著蛋彩來使用。我們現在似乎正處於這個階段。 Lisp 被大學,研究室和某些頂尖的公司所使用。同時,從 Lisp 借鑑的思想越來越多地出現在主流語言中:交互式編程環境 (interactive programming environment)、垃圾回收(garbage collection)、運行期型別 (run-time typing),僅舉其中幾個。 強大的工具正降低探索的風險。這對程式設計師來說是好消息,因爲意味者我們可以從事更有野心的項目。油畫的確有這個效果。採用油畫後的時期正是繪畫的黃金時期。類似的跡象正在程式設計的領域中發生。 第二章:歡迎來到 Lisp¶本章的目的是讓你儘快開始編程。本章結束時,你會掌握足夠多的 Common Lisp 知識來開始寫程式。 2.1 形式 (Form)¶人可以通過實踐來學習一件事,這對於 Lisp 來說特別有效,因爲 Lisp 是一門交互式的語言。任何 Lisp 系統都含有一個交互式的前端,叫做頂層(toplevel)。你在頂層輸入 Lisp 表達式,而系統會顯示它們的值。 Lisp 通常會打印一個提示符告訴你,它正在等待你的輸入。許多 Common Lisp 的實現用 一個最簡單的 Lisp 表達式是整數。如果我們在提示符後面輸入 > 1
1
>
系統會打印出它的值,接著印出另一個提示符,告訴你它在等待更多的輸入。 在這個情況裡,打印的值與輸入的值相同。數字 > (+ 2 3)
5
在表達式 在日常生活中,我們會把表達式寫作 舉例來說,我們想把三個數加起來,用日常生活的表示法,要寫兩次 2 + 3 + 4
而在 Lisp 裡,只需要增加一個實參: (+ 2 3 4)
日常生活中用 > (+)
0
> (+ 2)
2
> (+ 2 3)
5
> (+ 2 3 4)
9
> (+ 2 3 4 5)
14
由於運算子可接受不定數量的實參,我們需要用括號來標明表達式的開始與結束。 表達式可以巢狀。即表達式裡的實參,可以是另一個複雜的表達式: > (/ (- 7 1) (- 4 2))
3
上面的表達式用中文來說是, (七減一) 除以 (四減二) 。 Lisp 表示法另一個美麗的地方是:它就是如此簡單。所有的 Lisp 表達式,要麼是 2 (+ 2 3) (+ 2 3 4) (/ (- 7 1) (- 4 2))
稍後我們將理解到,所有的 Lisp 程式都採用這種形式。而像是 C 這種語言,有著更複雜的語法:算術表達式採用中序表示法;函數呼叫採用某種前序表示法,實參用逗號隔開;表達式用分號隔開;而一段程式用大括號隔開。 在 Lisp 裡,我們用單一的表示法,來表達所有的概念。 2.2 求值 (Evaluation)¶上一小節中,我們在頂層輸入表達式,然後 Lisp 顯示它們的值。在這節裡我們深入理解一下表達式是如何被求值的。 在 Lisp 裡, 當 Lisp 對函數呼叫求值時,它做下列兩個步驟:
如果實參本身是函數呼叫的話,上述規則同樣適用。以下是當
但不是所有的 Common Lisp 運算子都是函數,不過大部分是。函數呼叫都是這麼求值。由左至右對實參求值,將它們的數值傳入函數,來返回整個表達式的值。這稱爲 Common Lisp 的求值規則。 Note 逃離麻煩 如果你試著輸入 Lisp 不能理解的東西,它會打印一個錯誤訊息,接著帶你到一種叫做中斷迴圈(break loop)的頂層。
中斷迴圈給予有經驗的程式設計師一個機會,來找出錯誤的原因,不過最初你只會想知道如何從中斷迴圈中跳出。
如何返回頂層取決於你所使用的 Common Lisp 實現。在這個假定的實現環境中,輸入 > (/ 1 0)
Error: Division by zero
Options: :abort, :backtrace
>> :abort
>
附錄 A 示範了如何除錯 Lisp 程式,並給出一些常見的錯誤例子。 一個不遵守 Common Lisp 求值規則的運算子是 > (quote (+ 3 5))
(+ 3 5)
爲了方便起見,Common Lisp 定義 > '(+ 3 5)
(+ 3 5)
使用縮寫 Lisp 提供 2.3 資料 (Data)¶Lisp 提供了所有在其他語言找的到的,以及其他語言所找不到的資料型別。一個我們已經使用過的型別是整數(integer),整數用一系列的數字來表示,比如:
有兩個通常在別的語言所找不到的 Lisp 資料型別是符號(symbol)與列表(lists),符號是英語的單詞 (words)。無論你怎麼輸入,通常會被轉換爲大寫: > 'Artichoke
ARTICHOKE
符號(通常)不對自身求值,所以要是想引用符號,應該像上例那樣用 列表是由被括號包住的零個或多個元素來表示。元素可以是任何型別,包含列表本身。使用列表必須要引用,不然 Lisp 會以爲這是個函數呼叫: > '(my 3 "Sons")
(MY 3 "Sons")
> '(the list (a b c) has 3 elements)
(THE LIST (A B C) HAS 3 ELEMENTS)
注意引號保護了整個表達式(包含內部的子表達式)被求值。 你可以呼叫 > (list 'my (+ 2 1) "Sons")
(MY 3 "Sons")
我們現在來到領悟 Lisp 最卓越特性的地方之一。Lisp的程式是用列表來表示的。如果實參的優雅與彈性不能說服你 Lisp 表示法是無價的工具,這裡應該能使你信服。這代表著 Lisp 程式可以寫出 Lisp 程式。 Lisp 程式設計師可以(並且經常)寫出能爲自己寫程式的程式。 不過得到第 10 章,我們才來考慮這種程式,但現在了解到列表和表達式的關係是非常重要的,而不是被它們搞混。這也就是爲什麼我們需要 > (list '(+ 2 1) (+ 2 1))
((+ 2 1) 3)
這裡第一個實參被引用了,所以產生一個列表。第二個實參沒有被引用,視爲函數呼叫,經求值後得到一個數字。 在 Common Lisp 裡有兩種方法來表示空列表。你可以用一對不包括任何東西的括號來表示,或用符號 > ()
NIL
> nil
NIL
你不需要引用 2.4 列表操作 (List Operations)¶用函數 > (cons 'a '(b c d))
(A B C D)
可以通過把新元素建立在空表之上,來構造一個新列表。上一節所看到的函數 > (cons 'a (cons 'b nil))
(A B)
> (list 'a 'b)
(A B)
取出列表元素的基本函數是 > (car '(a b c))
A
> (cdr '(a b c))
(B C)
你可以把 > (car (cdr (cdr '(a b c d))))
C
不過,你可以用更簡單的 > (third '(a b c d))
C
2.5 真與假 (Truth)¶在 Common Lisp 裡,符號 > (listp '(a b c))
T
函數的返回值將會被解釋成邏輯 邏輯 > (listp 27)
NIL
由於 > (null nil)
T
而如果實參是邏輯 > (not nil)
T
在 Common Lisp 裡,最簡單的條件式是 > (if (listp '(a b c))
(+ 1 2)
(+ 5 6))
3
> (if (listp 27)
(+ 1 2)
(+ 5 6))
11
與 > (if (listp 27)
(+ 1 2))
NIL
雖然 > (if 27 1 2)
1
邏輯運算子 > (and t (+ 1 2))
3
如果其中一個實參爲 以上這兩個運算子稱爲宏。宏和特殊的運算子一樣,可以繞過一般的求值規則。第十章解釋了如何編寫你自己的宏。 2.6 函數 (Functions)¶你可以用 > (defun our-third (x)
(car (cdr (cdr x))))
OUR-THIRD
第一個實參說明此函數的名稱將是 定義的剩餘部分, > (our-third '(a b c d))
C
既然我們已經討論過了變數,理解符號是什麼就更簡單了。符號是變數的名字,符號本身就是以物件的方式存在。這也是爲什麼符號,必須像列表一樣被引用。列表必須被引用,不然會被視爲程式碼。符號必須要被引用,不然會被當作變數。 你可以把函數定義想成廣義版的 Lisp 表達式。下面的表達式測試 > (> (+ 1 4) 3)
T
通過將這些數字替換爲變數,我們可以寫個函數,測試任兩數之和是否大於第三個數: > (defun sum-greater (x y z)
(> (+ x y) z))
SUM-GREATER
> (sum-greater 1 4 3)
T
Lisp 不對程式、過程以及函數作區別。函數做了所有的事情(事實上,函數是語言的主要部分)。如果你想要把你的函數之一作爲主函數(main function),可以這麼做,但平常你就能在頂層中呼叫任何函數。這表示當你編程時,你可以把程式拆分成一小塊一小塊地來做除錯。 2.7 遞迴 (Recursion)¶上一節我們所定義的函數,呼叫了別的函數來幫它們做事。比如 > (defun our-member (obj lst)
(if (null lst)
nil
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst)))))
OUR-MEMBER
謂詞 > (our-member 'b '(a b c))
(B C)
> (our-member 'z '(a b c))
NIL
下面是
當你想要了解遞迴函數是怎麼工作時,把它翻成這樣的敘述有助於你理解。 起初,許多人覺得遞迴函數很難理解。大部分的理解難處,來自於對函數使用了錯誤的比喻。人們傾向於把函數理解爲某種機器。原物料像實參一樣抵達;某些工作委派給其它函數;最後組裝起來的成品,被作爲返回值運送出去。如果我們用這種比喻來理解函數,那遞迴就自相矛盾了。機器怎可以把工作委派給自己?它已經在忙碌中了。 較好的比喻是,把函數想成一個處理的過程。在過程裡,遞迴是在自然不過的事情了。日常生活中我們經常看到遞迴的過程。舉例來說,假設一個歷史學家,對歐洲歷史上的人口變化感興趣。研究文獻的過程很可能是:
過程是很容易理解的,而且它是遞迴的,因爲第三個步驟可能帶出一個或多個同樣的過程。 所以,別把 2.8 閱讀 Lisp (Reading Lisp)¶上一節我們所定義的 答案是,你不需要這麼做。 Lisp 程式設計師用縮排來閱讀及編寫程式,而不是括號。當他們在寫程式時,他們讓文字編輯器顯示哪個括號該與哪個匹配。任何好的文字編輯器,特別是 Lisp 系統自帶的,都應該能做到括號匹配(paren-matching)。在這種編輯器中,當你輸入一個括號時,編輯器指出與其匹配的那一個。如果你的編輯器不能匹配括號,別用了,想想如何讓它做到,因爲沒有這個功能,你根本不可能編 Lisp 程式 [1] 。 有了好的編輯器之後,括號匹配不再會是問題。而且由於 Lisp 縮排有通用的慣例,閱讀程式也不是個問題。因爲所有人都使用一樣的習慣,你可以忽略那些括號,通過縮排來閱讀程式。 任何有經驗的 Lisp 黑客,會發現如果是這樣的 (defun our-member (obj lst) (if (null lst) nil (if
(eql (car lst) obj) lst (our-member obj (cdr lst)))))
但如果程式適當地縮排時,他就沒有問題了。可以忽略大部分的括號而仍能讀懂它: defun our-member (obj lst)
if null lst
nil
if eql (car lst) obj
lst
our-member obj (cdr lst)
事實上,這是你在紙上寫 Lisp 程式的實用方法。等輸入程式至計算機的時候,可以利用編輯器匹配括號的功能。 2.9 輸入輸出 (Input and Output)¶到目前爲止,我們已經利用頂層偷偷使用了 I/O 。對實際的交互程式來說,這似乎還是不太夠。在這一節,我們來看幾個輸入輸出的函數。 最普遍的 Common Lisp 輸出函數是 > (format t "~A plus ~A equals ~A. ~%" 2 3 (+ 2 3))
2 plus 3 equals 5.
NIL
注意到有兩個東西被打印出來。第一行是
標準的輸入函數是 (defun askem (string)
(format t "~A" string)
(read))
它的行爲如下: > (askem "How old are you?")
How old are you?29
29
記住 第二件關於
在之前的每一節中,我們堅持所謂“純粹的” Lisp ── 即沒有副作用的 Lisp 。副作用是指,表達式被求值後,對外部世界的狀態做了某些改變。當我們對一個如 當我們想要寫沒有副作用的程式時,則定義多個表達式的函數主體就沒有意義了。最後一個表達式的值,會被當成函數的返回值,而之前表達式的值都被捨棄了。如果這些表達式沒有副作用,你沒有任何理由告訴 Lisp ,爲什麼要去對它們求值。 2.10 變數 (Variables)¶
> (let ((x 1) (y 2))
(+ x y))
3
一個 一組變數與數值之後,是一個有表達式的函數體,表達式依序被求值。但這個例子裡,只有一個表達式,呼叫 (defun ask-number ()
(format t "Please enter a number. ")
(let ((val (read)))
(if (numberp val)
val
(ask-number))))
這個函數創建了變數 如果使用者不是輸入一個數字, > (ask-number)
Please enter a number. a
Please enter a number. (ho hum)
Please enter a number. 52
52
我們已經看過的這些變數都叫做區域變數。它們只在特定的上下文裡有效。另外還有一種變數叫做全局變數(global variable),是在任何地方都是可視的。 [2] 你可以給 > (defparameter *glob* 99)
*GLOB*
全局變數在任何地方都可以存取,除了在定義了相同名字的區域變數的表達式裡。爲了避免這種情形發生,通常我們在給全局變數命名時,以星號作開始與結束。剛才我們創造的變數可以唸作 “星-glob-星” (star-glob-star)。 你也可以用 (defconstant limit (+ *glob* 1))
我們不需要給常數一個獨一無二的名字,因爲如果有相同名字存在,就會有錯誤產生 (error)。如果你想要檢查某些符號,是否爲一個全局變數或常數,使用 > (boundp '*glob*)
T
2.11 賦值 (Assignment)¶在 Common Lisp 裡,最普遍的賦值運算子(assignment operator)是 > (setf *glob* 98)
98
> (let ((n 10))
(setf n 2)
n)
2
如果 > (setf x (list 'a 'b 'c))
(A B C)
也就是說,通過賦值,你可以隱式地創建全局變數。
不過,一般來說,還是使用 你不僅可以給變數賦值。傳入 > (setf (car x) 'n)
N
> x
(N B C)
(setf a 'b
c 'd
e 'f)
等同於依序呼叫三個單獨的 (setf a 'b)
(setf c 'd)
(setf e 'f)
2.12 函數式編程 (Functional Programming)¶函數式編程意味著撰寫利用返回值而工作的程式,而不是修改東西。它是 Lisp 的主流範式。大部分 Lisp 的內建函數被呼叫是爲了取得返回值,而不是副作用。 舉例來說,函數 > (setf lst '(c a r a t))
(C A R A T)
> (remove 'a lst)
(C R T)
爲什麼不乾脆說 > lst
(C A R A T)
若你真的想從列表裡移除某些東西怎麼辦?在 Lisp 通常你這麼做,把這個列表當作實參,傳入某個函數,並使用 (setf x (remove 'a x))
函數式編程本質上意味著避免使用如 完全不用到副作用是很不方便的。然而,隨著你進一步閱讀,會驚訝地發現需要用到副作用的地方很少。副作用用得越少,你就更上一層樓。 函數式編程最重要的優點之一是,它允許交互式測試(interactive testing)。在純函數式的程式裡,你可以測試每個你寫的函數。如果它返回你預期的值,你可以有信心它是對的。這額外的信心,集結起來,會產生巨大的差別。當你改動了程式裡的任何一個地方,會得到即時的改變。而這種即時的改變,使我們有一種新的編程風格。類比於電話與信件,讓我們有一種新的通訊方式。 2.13 迭代 (Iteration)¶當我們想重複做一些事情時,迭代比遞迴來得更自然。典型的例子是用迭代來產生某種表格。這個函數 (defun show-squares (start end)
(do ((i start (+ i 1)))
((> i end) 'done)
(format t "~A ~A~%" i (* i i))))
列印從 > (show-squares 2 5)
2 4
3 9
4 16
5 25
DONE
(variable initial update)
其中 variable 是一個符號, initial 和 update 是表達式。最初每個變數會被賦予 initial 表達式的值;每一次迭代時,會被賦予 update 表達式的值。在 第二個傳給
作爲對比,以下是遞迴版本的 (defun show-squares (i end)
(if (> i end)
'done
(progn
(format t "~A ~A~%" i (* i i))
(show-squares (+ i 1) end))))
唯一的新東西是 爲了處理某些特殊情況, Common Lisp 有更簡單的迭代運算子。舉例來說,要遍歷列表的元素,你可能會使用 (defun our-length (lst)
(let ((len 0))
(dolist (obj lst)
(setf len (+ len 1)))
len))
這裡 (defun our-length (lst)
(if (null lst)
0
(+ (our-length (cdr lst)) 1)))
也就是說,如果列表是空表,則長度爲 2.14 函數作爲物件 (Functions as Objects)¶函數在 Lisp 裡,和符號、字串或列表一樣,是稀鬆平常的物件。如果我們把函數的名字傳給 > (function +)
#<Compiled-Function + 17BA4E>
這看起來很奇怪的返回值,是在典型的 Common Lisp 實現裡,函數可能的打印表示法。 到目前爲止,我們僅討論過,不管是 Lisp 打印它們,還是我們輸入它們,看起來都是一樣的物件。但這個慣例對函數不適用。一個像是 如同我們可以用 > #'+
#<Compiled-Function + 17BA4E>
這個縮寫稱之爲升引號(sharp-quote)。 和別種物件類似,可以把函數當作實參傳入。有個接受函數作爲實參的函數是 > (apply #'+ '(1 2 3))
6
> (+ 1 2 3)
6
> (apply #'+ 1 2 '(3 4 5))
15
函數 > (funcall #'+ 1 2 3)
6
Note 什麼是
在 Common Lisp 裡,你可以用列表來表達函數, 函數在內部會被表示成獨特的函數物件。因此不再需要 lambda 了。 如果需要把函數記爲
而不是
也是可以的。 但 Lisp 程式設計師習慣用符號
要直接引用整數,我們使用一系列的數字;要直接引用一個函數,我們使用所謂的lambda 表達式。一個 下面的 (lambda (x y)
(+ x y))
列表 一個 > ((lambda (x) (+ x 100)) 1)
101
而通過在 > (funcall #'(lambda (x) (+ x 100))
1)
2.15 型別 (Types)¶Lisp 處理型別的方法非常靈活。在很多語言裡,變數是有型別的,得宣告變數的型別才能使用它。在 Common Lisp 裡,數值才有型別,而變數沒有。你可以想像每個物件,都貼有一個標明其型別的標籤。這種方法叫做顯式型別(manifest typing)。你不需要宣告變數的型別,因爲變數可以存放任何型別的物件。 雖然從來不需要宣告型別,但出於效率的考量,你可能會想要宣告變數的型別。型別宣告在第 13.3 節時討論。 Common Lisp 的內建型別,組成了一個類別的層級。物件總是不止屬於一個型別。舉例來說,數字 27 的型別,依普遍性的增加排序,依序是 函數 > (typep 27 'integer)
T
我們會在遇到各式內建型別時來討論它們。 2.16 展望 (Looking Forward)¶本章僅談到 Lisp 的表面。然而,一種非比尋常的語言形象開始出現了。首先,這個語言用單一的語法,來表達所有的程式結構。語法基於列表,列表是一種 Lisp 物件。函數本身也是 Lisp 物件,函數能用列表來表示。而 Lisp 本身就是 Lisp 程式。幾乎所有你定義的函數,與內建的 Lisp 函數沒有任何區別。 如果你對這些概念還不太了解,不用擔心。 Lisp 介紹了這麼多新穎的概念,在你能駕馭它們之前,得花時間去熟悉它們。不過至少要了解一件事:在這些概念當中,有著優雅到令人吃驚的概念。 Richard Gabriel 曾經半開玩笑的說, C 是拿來寫 Unix 的語言。我們也可以說, Lisp 是拿來寫 Lisp 的語言。但這是兩種不同的論述。一個可以用自己編寫的語言和一種適合編寫某些特定型別應用的語言,是有著本質上的不同。這開創了新的編程方法:你不但在語言之中編程,還把語言改善成適合程式的語言。如果你想了解 Lisp 編程的本質,理解這個概念是個好的開始。 Chapter 2 總結 (Summary)¶
Chapter 2 習題 (Exercises)¶
(a) (+ (- 5 1) (+ 3 7))
(b) (list 1 (+ 2 3))
(c) (if (listp 1) (+ 1 2) (+ 3 4))
(d) (list (and (listp 3) t) (+ 1 2))
(a) (defun enigma (x)
(and (not (null x))
(or (null (car x))
(enigma (cdr x)))))
(b) (defun mystery (x y)
(if (null y)
nil
(if (eql (car y) x)
0
(let ((z (mystery x (cdr y))))
(and z (+ z 1))))))
(a) > (car (x (cdr '(a (b c) d))))
B
(b) > (x 13 (/ 1 0))
13
(c) > (x #'list 1 nil)
(1)
(a) (defun summit (lst)
(remove nil lst)
(apply #'+ lst))
(b) (defun summit (lst)
(let ((x (car lst)))
(if (null x)
(summit (cdr lst))
(+ x (summit (cdr lst))))))
腳註
第三章:列表¶列表是 Lisp 的基本資料結構之一。在最早的 Lisp 方言裡,列表是唯一的資料結構: “Lisp” 這個名字起初是 “LISt Processor” 的縮寫。但 Lisp 已經超越這個縮寫很久了。 Common Lisp 是一個有著各式各樣資料結構的通用性程式語言。 Lisp 程式開發通常呼應著開發 Lisp 語言自身。在最初版本的 Lisp 程式,你可能使用很多列表。然而之後的版本,你可能換到快速、特定的資料結構。本章描述了你可以用列表所做的很多事情,以及使用它們來示範一些普遍的 Lisp 概念。 3.1 構造 (Conses)¶在 2.4 節我們介紹了 Cons 物件提供了一個方便的表示法,來表示任何型別的物件。一個 Cons 物件裡的一對指標,可以指向任何型別的物件,包括 Cons 物件本身。它利用到我們之後可以用 我們往往不會把列表想成是成對的,但它們可以這樣被定義。任何非空的列表,都可以被視爲一對由列表第一個元素及列表其餘元素所組成的列表。 Lisp 列表體現了這個概念。我們使用 Cons 的一半來指向列表的第一個元素,然後用另一半指向列表其餘的元素(可能是別的 Cons 或 當我們想在 > (setf x (cons 'a nil))
(A)
![]() 圖 3.1 一個元素的列表 產生的列表由一個 Cons 所組成,見圖 3.1。這種表達 Cons 的方式叫做箱子表示法 (box notation),因爲每一個 Cons 是用一個箱子表示,內含一個 > (car x)
A
> (cdr x)
NIL
當我們構造一個多元素的列表時,我們得到一串 Cons (a chain of conses): > (setf y (list 'a 'b 'c))
(A B C)
產生的結構見圖 3.2。現在當我們想得到列表的 ![]() 圖 3.2 三個元素的列表 > (cdr y)
(B C)
在一個有多個元素的列表中, 一個列表可以有任何型別的物件作爲元素,包括另一個列表: > (setf z (list 'a (list 'b 'c) 'd))
(A (B C) D)
當這種情況發生時,它的結構如圖 3.3 所示;第二個 Cons 的 > (car (cdr z))
(B C)
![]() 圖 3.3 巢狀列表 前兩個我們構造的列表都有三個元素;只不過 如果參數是一個 Cons 物件,函數 (defun our-listp (x)
(or (null x) (consp x)))
因爲所有不是 Cons 物件的東西,就是一個原子 (atom),判斷式 (defun our-atom (x) (not (consp x)))
注意, 3.2 等式 (Equality)¶每一次你呼叫 > (eql (cons 'a nil) (cons 'a nil))
NIL
如果我們也可以詢問兩個列表是否有相同元素,那就很方便了。 Common Lisp 提供了這種目的另一個判斷式: > (setf x (cons 'a nil))
(A)
> (eql x x)
T
本質上 > (equal x (cons 'a nil))
T
這個判斷式對非列表結構的別種物件也有效,但一種僅對列表有效的版本可以這樣定義: > (defun our-equal (x y)
(or (eql x y)
(and (consp x)
(consp y)
(our-equal (car x) (car y))
(our-equal (cdr x) (cdr y)))))
這個定義意味著,如果某個 勘誤: 這個版本的 3.3 爲什麼 Lisp 沒有指標 (Why Lisp Has No Pointers)¶一個理解 Lisp 的祕密之一是意識到變數是有值的,就像列表有元素一樣。如同 Cons 物件有指標指向他們的元素,變數有指標指向他們的值。 你可能在別的語言中使用過顯式指標 (explicitly pointer)。在 Lisp,你永遠不用這麼做,因爲語言幫你處理好指標了。我們已經在列表看過這是怎麼實現的。同樣的事情發生在變數身上。舉例來說,假設我們想要把兩個變數設成同樣的列表: > (setf x '(a b c))
(A B C)
> (setf y x)
(A B C)
![]() 圖 3.4 兩個變數設爲相同的列表 當我們把 > (eql x y)
T
Lisp 沒有指標的原因是因爲每一個值,其實概念上來說都是一個指標。當你賦一個值給變數或將這個值存在資料結構中,其實被儲存的是指向這個值的指標。當你要取得變數的值,或是存在資料結構中的內容時, Lisp 返回指向這個值的指標。但這都在檯面下發生。你可以不加思索地把值放在結構裡,或放“在”變數裡。 爲了效率的原因, Lisp 有時會選擇一個折衷的表示法,而不是指標。舉例來說,因爲一個小整數所需的記憶體空間,少於一個指標所需的空間,一個 Lisp 實現可能會直接處理這個小整數,而不是用指標來處理。但基本要點是,程式設計師預設可以把任何東西放在任何地方。除非你宣告你不願這麼做,不然你能夠在任何的資料結構,存放任何型別的物件,包括結構本身。 3.4 建立列表 (Building Lists)¶![]() 圖 3.5 複製的結果 函數 > (setf x '(a b c)
y (copy-list x))
(A B C)
圖 3.5 展示出結果的結構; 返回值像是有著相同乘客的新公交。我們可以把 (defun our-copy-list (lst)
(if (atom lst)
lst
(cons (car lst) (our-copy-list (cdr lst)))))
這個定義暗示著 最後,函數 > (append '(a b) '(c d) 'e)
(A B C D . E)
通過這麼做,它複製所有的參數,除了最後一個 3.5 範例:壓縮 (Example: Compression)¶作爲一個例子,這節將示範如何實現簡單形式的列表壓縮。這個算法有一個令人印象深刻的名字,遊程編碼(run-length encoding)。 (defun compress (x)
(if (consp x)
(compr (car x) 1 (cdr x))
x))
(defun compr (elt n lst)
(if (null lst)
(list (n-elts elt n))
(let ((next (car lst)))
(if (eql next elt)
(compr elt (+ n 1) (cdr lst))
(cons (n-elts elt n)
(compr next 1 (cdr lst)))))))
(defun n-elts (elt n)
(if (> n 1)
(list n elt)
elt))
圖 3.6 遊程編碼 (Run-length encoding):壓縮 在餐廳的情境下,這個算法的工作方式如下。一個女服務生走向有四個客人的桌子。“你們要什麼?” 她問。“我要特餐,” 第一個客人說。 “我也是,” 第二個客人說。“聽起來不錯,” 第三個客人說。每個人看著第四個客人。 “我要一個 cilantro soufflé,” 他小聲地說。 (譯註:蛋奶酥上面灑香菜跟醬料) 瞬息之間,女服務生就轉身踩著高跟鞋走回櫃檯去了。 “三個特餐,” 她大聲對廚師說,“還有一個香菜蛋奶酥。” 圖 3.6 示範了如何實現這個壓縮列表演算法。函數 > (compress '(1 1 1 0 1 0 0 0 0 1))
((3 1) 0 1 (4 0) 1)
當相同的元素連續出現好幾次,這個連續出現的序列 (sequence)被一個列表取代,列表指明出現的次數及出現的元素。 主要的工作是由遞迴函數 要復原一個壓縮的列表,我們呼叫 > (uncompress '((3 1) 0 1 (4 0) 1))
(1 1 1 0 1 0 0 0 0 1)
(defun uncompress (lst)
(if (null lst)
nil
(let ((elt (car lst))
(rest (uncompress (cdr lst))))
(if (consp elt)
(append (apply #'list-of elt)
rest)
(cons elt rest)))))
(defun list-of (n elt)
(if (zerop n)
nil
(cons elt (list-of (- n 1) elt))))
圖 3.7 遊程編碼 (Run-length encoding):解壓縮 這個函數遞迴地遍歷這個壓縮列表,逐字複製原子並呼叫 > (list-of 3 'ho)
(HO HO HO)
我們其實不需要自己寫 圖 3.6 跟 3.7 這種寫法不是一個有經驗的Lisp 程式設計師用的寫法。它的效率很差,它沒有儘可能的壓縮,而且它只對由原子組成的列表有效。在幾個章節內,我們會學到解決這些問題的技巧。 載入程式
在這節的程式是我們第一個實質的程式。
當我們想要寫超過數行的函數時,
通常我們會把程式寫在一個檔案,
然後使用 load 讓 Lisp 讀取函數的定義。
如果我們把圖 3.6 跟 3.7 的程式,
存在一個檔案叫做,“compress.lisp”然後輸入
(load "compress.lisp")
到頂層,或多或少的,
我們會像在直接輸入頂層一樣得到同樣的效果。
注意:在某些實現中,Lisp 檔案的擴展名會是“.lsp”而不是“.lisp”。
3.6 存取 (Access)¶Common Lisp 有額外的存取函數,它們是用 > (nth 0 '(a b c))
A
而要找到第 > (nthcdr 2 '(a b c))
(C)
兩個函數幾乎做一樣的事; (defun our-nthcdr (n lst)
(if (zerop n)
lst
(our-nthcdr (- n 1) (cdr lst))))
函數 函數 > (last '(a b c))
(C)
這跟取得最後一個元素不一樣。要取得列表的最後一個元素,你要取得 Common Lisp 定義了函數
此外, Common Lisp 定義了像是 3.7 映射函數 (Mapping Functions)¶Common Lisp 提供了數個函數來對一個列表的元素做函數呼叫。最常使用的是 > (mapcar #'(lambda (x) (+ x 10))
'(1 2 3))
(11 12 13)
> (mapcar #'list
'(a b c)
'(1 2 3 4))
((A 1) (B 2) (C 3))
相關的 > (maplist #'(lambda (x) x)
'(a b c))
((A B C) (B C) (C))
其它的映射函數,包括 3.8 樹 (Trees)¶Cons 物件可以想成是二元樹,
![]() 圖 3.8 二元樹 (Binary Tree) Common Lisp 有幾個內建的操作樹的函數。舉例來說, (defun our-copy-tree (tr)
(if (atom tr)
tr
(cons (our-copy-tree (car tr))
(our-copy-tree (cdr tr)))))
把這跟 36 頁的 沒有內部節點的二元樹沒有太大的用處。 Common Lisp 包含了操作樹的函數,不只是因爲我們需要樹這個結構,而是因爲我們需要一種方法,來操作列表及所有內部的列表。舉例來說,假設我們有一個這樣的列表: (and (integerp x) (zerop (mod x 2)))
而我們想要把各處的 > (substitute 'y 'x '(and (integerp x) (zerop (mod x 2))))
(AND (INTEGERP X) (ZEROP (MOD X 2)))
這個呼叫是無效的,因爲列表有三個元素,沒有一個元素是 > (subst 'y 'x '(and (integerp x) (zerop (mod x 2))))
(AND (INTEGERP Y) (ZEROP (MOD Y 2)))
如果我們定義一個 > (defun our-subst (new old tree)
(if (eql tree old)
new
(if (atom tree)
tree
(cons (our-subst new old (car tree))
(our-subst new old (cdr tree))))))
操作樹的函數通常有這種形式, 3.9 理解遞迴 (Understanding Recursion)¶學生在學習遞迴時,有時候是被鼓勵在紙上追蹤 (trace)遞迴程式呼叫 (invocation)的過程。 (288頁「譯註:附錄 A 追蹤與回溯」可以看到一個遞迴函數的追蹤過程。)但這種練習可能會誤導你:一個程式設計師在定義一個遞迴函數時,通常不會特別地去想函數的呼叫順序所導致的結果。 如果一個人總是需要這樣子思考程式,遞迴會是艱難的、沒有幫助的。遞迴的優點是它精確地讓我們更抽象地來檢視算法。你不需要考慮真正函數時所有的呼叫過程,就可以判斷一個遞迴函數是否是正確的。 要知道一個遞迴函數是否做它該做的事,你只需要問,它包含了所有的情況嗎?舉例來說,下面是一個尋找列表長度的遞迴函數: > (defun len (lst)
(if (null lst)
0
(+ (len (cdr lst)) 1)))
我們可以藉由檢查兩件事情,來確信這個函數是正確的:
如果這兩點是成立的,我們知道這個函數對於所有可能的列表都是正確的。 我們的定義顯然地滿足第一點:如果列表( 我們需要知道的就是這些。理解遞迴的祕密就像是處理括號一樣。你怎麼知道哪個括號對上哪個?你不需要這麼做。你怎麼想像那些呼叫過程?你不需要這麼做。 更複雜的遞迴函數,可能會有更多的情況需要討論,但是流程是一樣的。舉例來說, 41 頁的 第一個情況(長度零的列表)稱之爲基本用例( base case )。當一個遞迴函數不像你想的那樣工作時,通常是因爲基本用例是錯的。下面這個不正確的 (defun our-member (obj lst)
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst))))
我們需要初始一個 能夠判斷一個遞迴函數是否正確只不過是理解遞迴的上半場,下半場是能夠寫出一個做你想做的事情的遞迴函數。 6.9 節討論了這個問題。 3.10 集合 (Sets)¶列表是表示小集合的好方法。列表中的每個元素都代表了一個集合的成員: > (member 'b '(a b c))
(B C)
當 一般情況下, 一個 如果你在呼叫 > (member '(a) '((a) (z)) :test #'equal)
((A) (Z))
關鍵字參數總是選擇性添加的。如果你在一個呼叫中包含了任何的關鍵字參數,他們要擺在最後; 如果使用了超過一個的關鍵字參數,擺放的順序無關緊要。 另一個 > (member 'a '((a b) (c d)) :key #'car)
((A B) (C D))
在這個例子裡,我們詢問是否有一個元素的 如果我們想要使用兩個關鍵字參數,我們可以使用其中一個順序。下面這兩個呼叫是等價的: > (member 2 '((1) (2)) :key #'car :test #'equal)
((2))
> (member 2 '((1) (2)) :test #'equal :key #'car)
((2))
兩者都詢問是否有一個元素的 如果我們想要找到一個元素滿足任意的判斷式像是── > (member-if #'oddp '(2 3 4))
(3 4)
我們可以想像一個限制性的版本 (defun our-member-if (fn lst)
(and (consp lst)
(if (funcall fn (car lst))
lst
(our-member-if fn (cdr lst)))))
函數 > (adjoin 'b '(a b c))
(A B C)
> (adjoin 'z '(a b c))
(Z A B C)
通常的情況下它接受與 集合論中的並集 (union)、交集 (intersection)以及補集 (complement)的實現,是由函數 這些函數期望兩個(正好 2 個)列表(一樣接受與 > (union '(a b c) '(c b s))
(A C B S)
> (intersection '(a b c) '(b b c))
(B C)
> (set-difference '(a b c d e) '(b e))
(A C D)
因爲集閤中沒有順序的概念,這些函數不需要保留原本元素在列表被找到的順序。舉例來說,呼叫 3.11 序列 (Sequences)¶另一種考慮一個列表的方式是想成一系列有特定順序的物件。在 Common Lisp 裡,序列( sequences )包括了列表與向量 (vectors)。本節介紹了一些可以運用在列表上的序列函數。更深入的序列操作在 4.4 節討論。 函數 > (length '(a b c))
3
我們在 24 頁 (譯註:2.13節 要複製序列的一部分,我們使用 > (subseq '(a b c d) 1 2)
(B)
>(subseq '(a b c d) 1)
(B C D)
如果省略了第三個參數,子序列會從第二個參數給定的位置引用到序列尾端。 函數 > (reverse '(a b c))
(C B A)
一個迴文 (palindrome) 是一個正讀反讀都一樣的序列 —— 舉例來說, (defun mirror? (s)
(let ((len (length s)))
(and (evenp len)
(let ((mid (/ len 2)))
(equal (subseq s 0 mid)
(reverse (subseq s mid)))))))
來檢測是否是迴文: > (mirror? '(a b b a))
T
Common Lisp 有一個內建的排序函數叫做 > (sort '(0 2 1 3 8) #'>)
(8 3 2 1 0)
你要小心使用 使用 (defun nthmost (n lst)
(nth (- n 1)
(sort (copy-list lst) #'>)))
我們把整數減一因爲 (nthmost 2 '(0 2 1 3 8))
多努力一點,我們可以寫出這個函數的一個更有效率的版本。 函數 > (every #'oddp '(1 3 5))
T
> (some #'evenp '(1 2 3))
T
如果它們輸入多於一個序列時,判斷式必須接受與序列一樣多的元素作爲參數,而參數從所有序列中一次提取一個: > (every #'> '(1 3 5) '(0 2 4))
T
如果序列有不同的長度,最短的那個序列,決定需要測試的次數。 3.12 棧 (Stacks)¶用 Cons 物件來表示的列表,很自然地我們可以拿來實現下推棧 (pushdown stack)。這太常見了,以致於 Common Lisp 提供了兩個宏給堆疊使用: 兩個函數都是由 表達式
等同於
而表達式
等同於 (let ((x (car lst)))
(setf lst (cdr lst))
x)
所以,舉例來說: > (setf x '(b))
(B)
> (push 'a x)
(A B)
> x
(A B)
> (setf y x)
(A B)
> (pop x)
(A)
> x
(B)
> y
(A B)
以上,全都遵循上述由 ![]() 圖 3.9 push 及 pop 的效果 你可以使用 (defun our-reverse (lst)
(let ((acc nil))
(dolist (elt lst)
(push elt acc))
acc))
在這個版本,我們從一個空列表開始,然後把
> (let ((x '(a b)))
(pushnew 'c x)
(pushnew 'a x)
x)
(C A B)
在這裡, 3.13 點狀列表 (Dotted Lists)¶呼叫 (defun proper-list? (x)
(or (null x)
(and (consp x)
(proper-list? (cdr x)))))
至目前爲止,我們構造的列表都是正規列表。 然而, > (setf pair (cons 'a 'b))
(A . B)
因爲這個 Cons 物件不是一個正規列表,它用點狀表示法來顯示。在點狀表示法,每個 Cons 物件的 ![]() 圖3.10 一個成對的 Cons 物件 (A cons used as a pair) 一個非正規列表的 Cons 物件稱之爲點狀列表 (dotted list)。這不是個好名字,因爲非正規列表的 Cons 物件通常不是用來表示列表: 你也可以用點狀表示法表示正規列表,但當 Lisp 顯示一個正規列表時,它會使用普通的列表表示法: > '(a . (b . (c . nil)))
(A B C)
順道一提,注意列表由點狀表示法與圖 3.2 箱子表示法的關聯性。 還有一個過渡形式 (intermediate form)的表示法,介於列表表示法及純點狀表示法之間,對於 > (cons 'a (cons 'b (cons 'c 'd)))
(A B C . D)
![]() 圖 3.11 一個點狀列表 (A dotted list) 這樣的 Cons 物件看起來像正規列表,除了最後一個 cdr 前面有一個句點。這個列表的結構展示在圖 3.11 ; 注意它跟圖3.2 是多麼的相似。 所以實際上你可以這麼表示列表 (a . (b . nil))
(a . (b))
(a b . nil)
(a b)
雖然 Lisp 總是使用後面的形式,來顯示這個列表。 3.14 關聯列表 (Assoc-lists)¶用 Cons 物件來表示映射 (mapping)也是很自然的。一個由 Cons 物件組成的列表稱之爲關聯列表(assoc-listor alist)。這樣的列表可以表示一個翻譯的集合,舉例來說: > (setf trans '((+ . "add") (- . "subtract")))
((+ . "add") (- . "subtract"))
關聯列表很慢,但是在初期的程式中很方便。 Common Lisp 有一個內建的函數 > (assoc '+ trans)
(+ . "add")
> (assoc '* trans)
NIL
如果 我們可以定義一個受限版本的 (defun our-assoc (key alist)
(and (consp alist)
(let ((pair (car alist)))
(if (eql key (car pair))
pair
(our-assoc key (cdr alist))))))
和 3.15 範例:最短路徑 (Example: Shortest Path)¶圖 3.12 包含一個搜索網路中最短路徑的程式。函數 在這個範例中,節點用符號表示,而網路用含以下元素形式的關聯列表來表示: (node . neighbors) 所以由圖 3.13 展示的最小網路 (minimal network)可以這樣來表示:
(defun shortest-path (start end net)
(bfs end (list (list start)) net))
(defun bfs (end queue net)
(if (null queue)
nil
(let ((path (car queue)))
(let ((node (car path)))
(if (eql node end)
(reverse path)
(bfs end
(append (cdr queue)
(new-paths path node net))
net))))))
(defun new-paths (path node net)
(mapcar #'(lambda (n)
(cons n path))
(cdr (assoc node net))))
圖 3.12 廣度優先搜索(breadth-first search) ![]() 圖 3.13 最小網路 要找到從節點 > (cdr (assoc 'a min))
(B C)
圖 3.12 程式使用廣度優先的方式搜索網路。要使用廣度優先搜索,你需要維護一個含有未探索節點的佇列。每一次你到達一個節點,檢查這個節點是否是你要的。如果不是,你把這個節點的子節點加入佇列的尾端,並從佇列起始選一個節點,從這繼續搜索。藉由總是把較深的節點放在佇列尾端,我們確保網路一次被搜索一層。 圖 3.12 中的程式較不複雜地表示這個概念。我們不僅想要找到節點,還想保有我們怎麼到那的紀錄。所以與其維護一個具有節點的佇列 (queue),我們維護一個已知路徑的佇列,每個已知路徑都是一列節點。當我們從佇列取出一個元素繼續搜索時,它是一個含有佇列前端節點的列表,而不只是一個節點而已。 函數
因爲 > (shortest-path 'a 'd min)
(A C D)
這是佇列在我們連續呼叫 ((A))
((B A) (C A))
((C A) (C B A))
((C B A) (D C A))
((D C A) (D C B A))
在佇列中的第二個元素變成下一個佇列的第一個元素。佇列的第一個元素變成下一個佇列尾端元素的 圖 3.12 的程式,不是搜索一個網路最快的方法,但它給出了列表具有多功能的概念。在這個簡單的程式中,我們用三種不同的方式使用了列表:我們使用一個符號的列表來表示路徑,一個路徑的列表來表示在廣度優先搜索中的佇列 [4] ,以及一個關聯列表來表示網路本身。 3.16 垃圾 (Garbages)¶有很多原因可以使列表變慢。列表提供了循序存取而不是隨機存取,所以列表取出一個指定的元素比陣列慢,同樣的原因,錄音帶取出某些東西比在光盤上慢。電腦內部裡, Cons 物件傾向於用指標表示,所以走訪一個列表意味著走訪一系列的指標,而不是簡單地像陣列一樣增加索引值。但這兩個所花的代價與配置及回收 Cons 核 (cons cells)比起來小多了。 自動記憶體管理(Automatic memory management)是 Lisp 最有價值的特色之一。 Lisp 系統維護著一段記憶體稱之爲堆疊(Heap)。系統持續追蹤堆疊當中沒有使用的記憶體,把這些記憶體發放給新產生的物件。舉例來說,函數 如果記憶體永遠沒有釋放, Lisp 會因爲創建新物件把記憶體用完,而必須要關閉。所以系統必須週期性地通過搜索堆疊 (heap),尋找不需要再使用的記憶體。不需要再使用的記憶體稱之爲垃圾 (garbage),而清除垃圾的動作稱爲垃圾回收 (garbage collection或 GC)。 垃圾是從哪來的?讓我們來創造一些垃圾: > (setf lst (list 'a 'b 'c))
(A B C)
> (setf lst nil)
NIL
一開始我們呼叫 因爲我們沒有任何方法再存取列表,它也有可能是不存在的。我們不再有任何方式可以存取的物件叫做垃圾。系統可以安全地重新使用這三個 Cons 核。 這種管理記憶體的方法,給程式設計師帶來極大的便利性。你不用顯式地配置 (allocate)或釋放 (dellocate)記憶體。這也表示了你不需要處理因爲這麼做而可能產生的臭蟲。記憶體泄漏 (Memory leaks)以及迷途指標 (dangling pointer)在 Lisp 中根本不可能發生。 但是像任何的科技進步,如果你不小心的話,自動記憶體管理也有可能對你不利。使用及回收堆疊所帶來的代價有時可以看做 除非你很小心,不然很容易寫出過度顯式創建 cons 物件的程式。舉例來說, 當寫出 無論如何 consing 在原型跟實驗時是好的。而且如果你利用了列表給你帶來的靈活性,你有較高的可能寫出後期可存活下來的程式。 Chapter 3 總結 (Summary)¶
Chapter 3 習題 (Exercises)¶
(a) (a b (c d))
(b) (a (b (c (d))))
(c) (((a b) c) d)
(d) (a (b . c) d)
> (new-union '(a b c) '(b a d))
(A B C D)
> (occurrences '(a b a d a c d c a))
((A . 4) (C . 2) (D . 2) (B . 1))
> (pos+ '(7 5 1 4))
(7 6 3 7)
使用 (a) 遞迴 (b) 迭代 (c)
(a) cons
(b) list
(c) length (for lists)
(d) member (for lists; no keywords)
勘誤: 要解決 3.6 (b),你需要使用到 6.3 節的參數
> (showdots '(a b c))
(A . (B . (C . NIL)))
NIL
腳註
第四章:特殊資料結構¶在之前的章節裡,我們討論了列表,Lisp 最多功能的資料結構。本章將示範如何使用 Lisp 其它的資料結構:陣列(包含向量與字串),結構以及雜湊表。它們或許不像列表這麼靈活,但存取速度更快並使用了更少空間。 Common Lisp 還有另一種資料結構:實體(instance)。實體將在 11 章討論,講述 CLOS。 4.1 陣列 (Array)¶在 Common Lisp 裡,你可以呼叫 > (setf arr (make-array '(2 3) :initial-element nil))
#<Simple-Array T (2 3) BFC4FE>
Common Lisp 的陣列至少可以有七個維度,每個維度至多可以有 1023 個元素。
用 > (aref arr 0 0)
NIL
要替換陣列的某個元素,我們使用 > (setf (aref arr 0 0) 'b)
B
> (aref arr 0 0)
B
要表示字面常數的陣列(literal array),使用 #2a((b nil nil) (nil nil nil))
如果全局變數 > (setf *print-array* t)
T
> arr
#2A((B NIL NIL) (NIL NIL NIL))
如果我們只想要一維的陣列,你可以給 > (setf vec (make-array 4 :initial-element nil))
#(NIL NIL NIL NIL)
一維陣列又稱爲向量(vector)。你可以通過呼叫 > (vector "a" 'b 3)
#("a" b 3)
字面常數的陣列可以表示成 可以用 > (svref vec 0)
NIL
在 4.2 範例:二元搜索 (Example: Binary Search)¶作爲一個範例,這小節示範如何寫一個在排序好的向量裡搜索物件的函數。如果我們知道一個向量是排序好的,我們可以比(65頁) 圖 4.1 包含了一個這麼工作的函數。其實這兩個函數: (defun bin-search (obj vec)
(let ((len (length vec)))
(and (not (zerop len))
(finder obj vec 0 (- len 1)))))
(defun finder (obj vec start end)
(let ((range (- end start)))
(if (zerop range)
(if (eql obj (aref vec start))
obj
nil)
(let ((mid (+ start (round (/ range 2)))))
(let ((obj2 (aref vec mid)))
(if (< obj obj2)
(finder obj vec start (- mid 1))
(if (> obj obj2)
(finder obj vec (+ mid 1) end)
obj)))))))
圖 4.1: 搜索一個排序好的向量 如果要找的 如果我們插入下面這行至 (format t "~A~%" (subseq vec start (+ end 1)))
我們可以觀察被搜索的元素的數量,是每一步往左減半的: > (bin-search 3 #(0 1 2 3 4 5 6 7 8 9))
#(0 1 2 3 4 5 6 7 8 9)
#(0 1 2 3)
#(3)
3
4.3 字元與字串 (Strings and Characters)¶字串是字元組成的向量。我們用一系列由雙引號包住的字元,來表示一個字串常數,而字元 每個字元都有一個相關的整數 ── 通常是 ASCII 碼,但不一定是。在多數的 Lisp 實現裡,函數 字元比較函數 > (sort "elbow" #'char<)
"below"
由於字串是字元向量,序列與陣列的函數都可以用在字串。你可以用 > (aref "abc" 1)
#\b
但針對字串可以使用更快的 > (char "abc" 1)
#\b
可以使用 > (let ((str (copy-seq "Merlin")))
(setf (char str 3) #\k)
str)
如果你想要比較兩個字串,你可以使用通用的 > (equal "fred" "fred")
T
> (equal "fred" "Fred")
NIL
>(string-equal "fred" "Fred")
T
Common Lisp 提供大量的操控、比較字串的函數。收錄在附錄 D,從 364 頁開始。 有許多方式可以創建字串。最普遍的方式是使用 > (format nil "~A or ~A" "truth" "dare")
"truth or dare"
但若你只想把數個字串連結起來,你可以使用 > (concatenate 'string "not " "to worry")
"not to worry"
4.4 序列 (Sequences)¶在 Common Lisp 裡,序列型別包含了列表與向量(因此也包含了字串)。有些用在列表的函數,實際上是序列函數,包括 > (mirror? "abba")
T
我們已經看過四種用來取出序列元素的函數: 給列表使用的 > (elt '(a b c) 1)
B
針對特定型別的序列,特定的存取函數會比較快,所以使用 使用 (defun mirror? (s)
(let ((len (length s)))
(and (evenp len)
(do ((forward 0 (+ forward 1))
(back (- len 1) (- back 1)))
((or (> forward back)
(not (eql (elt s forward)
(elt s back))))
(> forward back))))))
這個版本也可用在列表,但這個實現更適合給向量使用。頻繁的對列表呼叫 許多序列函數接受一個或多個,由下表所列的標準關鍵字參數:
一個接受所有關鍵字參數的函數是 > (position #\a "fantasia")
1
> (position #\a "fantasia" :start 3 :end 5)
4
第二個例子我們要找在第四個與第六個字元間,第一個 如果我們給入 > (position #\a "fantasia" :from-end t)
7
我們得到最靠近結尾的
> (position 'a '((c d) (a b)) :key #'car)
1
那麼我們要找的是,元素的
> (position '(a b) '((a b) (c d)))
NIL
> (position '(a b) '((a b) (c d)) :test #'equal)
0
> (position 3 '(1 0 7 5) :test #'<)
2
使用 (defun second-word (str)
(let ((p1 (+ (position #\ str) 1)))
(subseq str p1 (position #\ str :start p1))))
返回字串中第一個單字空格後的第二個單字: > (second-word "Form follows function")
"follows"
要找到滿足謂詞的元素,其中謂詞接受一個實參,我們使用 > (position-if #'oddp '(2 3 4 5))
1
有許多相似的函數,如給序列使用的 > (find #\a "cat")
#\a
> (find-if #'characterp "ham")
#\h
不同於 通常一個 (find-if #'(lambda (x)
(eql (car x) 'complete))
lst)
可以更好的解讀爲 (find 'complete lst :key #'car)
函數 > (remove-duplicates "abracadabra")
"cdbra"
這個函數接受前表所列的所有關鍵字參數。 函數 (reduce #'fn '(a b c d))
等同於 (fn (fn (fn 'a 'b) 'c) 'd)
我們可以使用 > (reduce #'intersection '((b r a d 's) (b a d) (c a t)))
(A)
4.5 範例:解析日期 (Example: Parsing Dates)¶作爲序列操作的範例,本節示範了如何寫程式來解析日期。我們將編寫一個程式,可以接受像是 “16 Aug 1980” 的字串,然後返回一個表示日、月、年的整數列表。 (defun tokens (str test start)
(let ((p1 (position-if test str :start start)))
(if p1
(let ((p2 (position-if #'(lambda (c)
(not (funcall test c)))
str :start p1)))
(cons (subseq str p1 p2)
(if p2
(tokens str test p2)
nil)))
nil)))
(defun constituent (c)
(and (graphic-char-p c)
(not (char= c #\ ))))
圖 4.2 辨別符號 (token) 圖 4.2 裡包含了某些在這個應用裡所需的通用解析函數。第一個函數 > (tokens "ab12 3cde.f" #'alpha-char-p 0)
("ab" "cde" "f")
所有不滿足此函數的字元被視爲空白 ── 他們是語元的分隔符,但永遠不是語元的一部分。 函數 在 Common Lisp 裡,圖形字元是我們可見的字元,加上空白字元。所以如果我們用 > (tokens "ab12 3cde.f gh" #'constituent 0)
("ab12" "3cde.f" "gh")
則語元將會由空白區分出來。 圖 4.3 包含了特別爲解析日期打造的函數。函數 > (parse-date "16 Aug 1980")
(16 8 1980)
(defun parse-date (str)
(let ((toks (tokens str #'constituent 0)))
(list (parse-integer (first toks))
(parse-month (second toks))
(parse-integer (third toks)))))
(defconstant month-names
#("jan" "feb" "mar" "apr" "may" "jun"
"jul" "aug" "sep" "oct" "nov" "dec"))
(defun parse-month (str)
(let ((p (position str month-names
:test #'string-equal)))
(if p
(+ p 1)
nil)))
圖 4.3 解析日期的函數
如果需要自己寫程式來解析整數,也許可以這麼寫: (defun read-integer (str)
(if (every #'digit-char-p str)
(let ((accum 0))
(dotimes (pos (length str))
(setf accum (+ (* accum 10)
(digit-char-p (char str pos)))))
accum)
nil))
這個定義示範了在 Common Lisp 中,字元是如何轉成數字的 ── 函數 4.6 結構 (Structures)¶結構可以想成是豪華版的向量。假設你要寫一個程式來追蹤長方體。你可能會想用三個向量元素來表示長方體:高度、寬度及深度。與其使用原本的 (defun block-height (b) (svref b 0))
而結構可以想成是,這些函數通通都替你定義好了的向量。 要想定義結構,使用 (defstruct point
x
y)
這裡定義了一個 2.3 節提過, Lisp 程式可以寫出 Lisp 程式。這是目前所見的明顯例子之一。當你呼叫 每一個 (setf p (make-point :x 0 :y 0))
#S(POINT X 0 Y 0)
存取 > (point-x p)
0
> (setf (point-y p) 2)
2
> p
#S(POINT X 0 Y 2)
定義結構也定義了以結構爲名的型別。每個點的型別層級會是,型別 > (point-p p)
T
> (typep p 'point)
T
我們可以在本來的定義中,附上一個列表,含有欄位名及預設表達式,來指定結構欄位的預設值。 (defstruct polemic
(type (progn
(format t "What kind of polemic was it? ")
(read)))
(effect nil))
如果 > (make-polemic)
What kind of polemic was it? scathing
#S(POLEMIC :TYPE SCATHING :EFFECT NIL)
結構顯示的方式也可以控制,以及結構自動產生的存取函數的字首。以下是做了前述兩件事的 (defstruct (point (:conc-name p)
(:print-function print-point))
(x 0)
(y 0))
(defun print-point (p stream depth)
(format stream "#<~A, ~A>" (px p) (py p)))
函數 > (make-point)
#<0,0>
4.7 範例:二元搜索樹 (Example: Binary Search Tree)¶由於 ![]() 圖 4.4: 二元搜索樹 二元搜索樹是一種二元樹,給定某個排序函數,比如 圖 4.5 包含了二元搜索樹的插入與尋找的函數。基本的資料結構會是 (defstruct (node (:print-function
(lambda (n s d)
(format s "#<~A>" (node-elt n)))))
elt (l nil) (r nil))
(defun bst-insert (obj bst <)
(if (null bst)
(make-node :elt obj)
(let ((elt (node-elt bst)))
(if (eql obj elt)
bst
(if (funcall < obj elt)
(make-node
:elt elt
:l (bst-insert obj (node-l bst) <)
:r (node-r bst))
(make-node
:elt elt
:r (bst-insert obj (node-r bst) <)
:l (node-l bst)))))))
(defun bst-find (obj bst <)
(if (null bst)
nil
(let ((elt (node-elt bst)))
(if (eql obj elt)
bst
(if (funcall < obj elt)
(bst-find obj (node-l bst) <)
(bst-find obj (node-r bst) <))))))
(defun bst-min (bst)
(and bst
(or (bst-min (node-l bst)) bst)))
(defun bst-max (bst)
(and bst
(or (bst-max (node-r bst)) bst)))
圖 4.5 二元搜索樹:查詢與插入 一棵二元搜索樹可以是 > (setf nums nil)
NIL
> (dolist (x '(5 8 4 2 1 9 6 7 3))
(setf nums (bst-insert x nums #'<)))
NIL
圖 4.4 顯示了此時 我們可以使用 與 > (bst-find 12 nums #'<)
NIL
> (bst-find 4 nums #'<)
#<4>
這使我們可以區分出無法找到某物,以及成功找到 要找到二元搜索樹的最小及最大的元素是很簡單的。要找到最小的,我們沿著左子樹的路徑走,如同 > (bst-min nums)
#<1>
> (bst-max nums)
#<9>
要從二元搜索樹裡移除元素一樣很快,但需要更多程式碼。圖 4.6 示範了如何從二元搜索樹裡移除元素。 (defun bst-remove (obj bst <)
(if (null bst)
nil
(let ((elt (node-elt bst)))
(if (eql obj elt)
(percolate bst)
(if (funcall < obj elt)
(make-node
:elt elt
:l (bst-remove obj (node-l bst) <)
:r (node-r bst))
(make-node
:elt elt
:r (bst-remove obj (node-r bst) <)
:l (node-l bst)))))))
(defun percolate (bst)
(cond ((null (node-l bst))
(if (null (node-r bst))
nil
(rperc bst)))
((null (node-r bst)) (lperc bst))
(t (if (zerop (random 2))
(lperc bst)
(rperc bst)))))
(defun rperc (bst)
(make-node :elt (node-elt (node-r bst))
:l (node-l bst)
:r (percolate (node-r bst))))
圖 4.6 二元搜索樹:移除 勘誤: 此版 函數 > (setf nums (bst-remove 2 nums #'<))
#<5>
> (bst-find 2 nums #'<)
NIL
此時 ![]() 圖 4.7: 二元搜索樹 移除需要做更多工作,因爲從內部節點移除一個物件時,會留下一個空缺,需要由其中一個孩子來填補。這是 爲了要保持樹的平衡,如果有兩個孩子時, (defun bst-traverse (fn bst)
(when bst
(bst-traverse fn (node-l bst))
(funcall fn (node-elt bst))
(bst-traverse fn (node-r bst))))
圖 4.8 二元搜索樹:遍歷 一旦我們把一個物件集合插入至二元搜索樹時,中序遍歷會將它們由小至大排序。這是圖 4.8 中, > (bst-traverse #'princ nums)
13456789
NIL
(函數 本節所給出的程式,提供了一個二元搜索樹實現的腳手架。你可能想根據應用需求,來充實這個腳手架。舉例來說,這裡所給出的程式每個節點只有一個 二元搜索樹不僅是維護一個已排序物件的集合的方法。他們是否是最好的方法,取決於你的應用。一般來說,二元搜索樹最適合用在插入與刪除是均勻分佈的情況。有一件二元搜索樹不擅長的事,就是用來維護優先佇列(priority queues)。在一個優先佇列裡,插入也許是均勻分佈的,但移除總是在一個另一端。這會導致一個二元搜索樹變得不平衡,而我們期望的複雜度是 4.8 雜湊表 (Hash Table)¶第三章示範過列表可以用來表示集合(sets)與映射(mappings)。但當列表的長度大幅上升時(或是 10 個元素),使用雜湊表的速度比較快。你通過呼叫 > (setf ht (make-hash-table))
#<Hash-Table BF0A96>
和函數一樣,雜湊表總是用 一個雜湊表,與一個關聯列表類似,是一種表達對應關係的方式。要取出與給定鍵值有關的數值,我們呼叫 > (gethash 'color ht)
NIL
NIL
在這裡我們首次看到 Common Lisp 最突出的特色之一:一個表達式竟然可以返回多個數值。函數 大部分的實現會在頂層顯示一個函數呼叫的所有返回值,但僅期待一個返回值的程式,只會收到第一個返回值。 5.5 節會說明,程式如何接收多個返回值。 要把數值與鍵值作關聯,使用 > (setf (gethash 'color ht) 'red)
RED
現在如果我們再次呼叫 > (gethash 'color ht)
RED
T
第二個返回值證明,我們取得了一個真正儲存的物件,而不是預設值。 存在雜湊表的物件或鍵值可以是任何型別。舉例來說,如果我們要保留函數的某種訊息,我們可以使用雜湊表,用函數作爲鍵值,字串作爲詞條(entry): > (setf bugs (make-hash-table))
#<Hash-Table BF4C36>
> (push "Doesn't take keyword arguments."
(gethash #'our-member bugs))
("Doesn't take keyword arguments.")
由於 可以用雜湊表來取代用列表表示集合。當集合變大時,雜湊表的查詢與移除會來得比較快。要新增一個成員到用雜湊表所表示的集合,把 > (setf fruit (make-hash-table))
#<Hash-Table BFDE76>
> (setf (gethash 'apricot fruit) t)
T
然後要測試是否爲成員,你只要呼叫: > (gethash 'apricot fruit)
T
T
由於 要從集閤中移除一個物件,你可以呼叫 > (remhash 'apricot fruit)
T
返回值說明了是否有詞條被移除;在這個情況裡,有。 雜湊表有一個迭代函數: > (setf (gethash 'shape ht) 'spherical
(gethash 'size ht) 'giant)
GIANT
> (maphash #'(lambda (k v)
(format t "~A = ~A~%" k v))
ht)
SHAPE = SPHERICAL
SIZE = GIANT
COLOR = RED
NIL
雜湊表可以容納任何數量的元素,但當雜湊表空間用完時,它們會被擴張。如果你想要確保一個雜湊表,從特定數量的元素空間大小開始時,可以給
會返回一個預期存放五個元素的雜湊表。 和任何牽涉到查詢的結構一樣,雜湊表一定有某種比較鍵值的概念。預設是使用 > (setf writers (make-hash-table :test #'equal))
#<Hash-Table C005E6>
> (setf (gethash '(ralph waldo emerson) writers) t)
T
這是一個讓雜湊表變得有效率的取捨之一。有了列表,我們可以指定 大多數 Lisp 編程的取捨(或是生活,就此而論)都有這種特質。起初你想要事情進行得流暢,甚至賠上效率的代價。之後當程式變得沉重時,你犧牲了彈性來換取速度。 Chapter 4 總結 (Summary)¶
Chapter 4 習題 (Exercises)¶
> (quarter-turn #2A((a b) (c d)))
#2A((C A) (D B))
你會需要用到 361 頁的
(a) copy-list
(b) reverse(針對列表)
(a) 一個函數來複製這樣的樹(複製完的節點與本來的節點是不相等( `eql` )的)
(b) 一個函數,接受一個物件與這樣的樹,如果物件與樹中各節點的其中一個欄位相等時,返回真。
勘誤:
(a) 接受一個關聯列表,並返回一個對應的雜湊表。
(b) 接受一個雜湊表,並返回一個對應的關聯列表。
腳註
第五章:控制流¶2.2 節介紹過 Common Lisp 的求值規則,現在你應該很熟悉了。本章的運算子都有一個共同點,就是它們都違反了求值規則。這些運算子讓你決定在程式當中何時要求值。如果普通的函數呼叫是 Lisp 程式的樹葉的話,那這些運算子就是連結樹葉的樹枝。 5.1 區塊(Blocks)¶Common Lisp 有三個構造區塊(block)的基本運算子: > (progn
(format t "a")
(format t "b")
(+ 11 12))
ab
23
由於只返回最後一個表達式的值,代表著使用 一個 > (block head
(format t "Here we go.")
(return-from head 'idea)
(format t "We'll never see this."))
Here we go.
IDEA
呼叫 也有一個 > (block nil
(return 27))
27
許多接受一個表達式主體的 Common Lisp 運算子,皆隱含在一個叫做 > (dolist (x '(a b c d e))
(format t "~A " x)
(if (eql x 'c)
(return 'done)))
A B C
DONE
使用 (defun foo ()
(return-from foo 27))
在一個顯式或隱式的 使用 (defun read-integer (str)
(let ((accum 0))
(dotimes (pos (length str))
(let ((i (digit-char-p (char str pos))))
(if i
(setf accum (+ (* accum 10) i))
(return-from read-integer nil))))
accum))
68 頁的版本在構造整數之前,需檢查所有的字元。現在兩個步驟可以結合,因爲如果遇到非數字的字元時,我們可以捨棄計算結果。出現在主體的原子(atom)被解讀爲標籤(labels);把這樣的標籤傳給 > (tagbody
(setf x 0)
top
(setf x (+ x 1))
(format t "~A " x)
(if (< x 10) (go top)))
1 2 3 4 5 6 7 8 9 10
NIL
這個運算子主要用來實現其它的運算子,不是一般會用到的運算子。大多數迭代運算子都隱含在一個 如何決定要使用哪一種區塊建構子呢(block construct)?幾乎任何時候,你會使用 5.2 語境(Context)¶另一個我們用來區分表達式的運算子是 > (let ((x 7)
(y 2))
(format t "Number")
(+ x y))
Number
9
一個像是 概念上說,一個 > ((lambda (x) (+ x 1)) 3)
4
前述的 ((lambda (x y)
(format t "Number")
(+ x y))
7
2)
如果有關於 這個模型清楚的告訴我們,由 (let ((x 2)
(y (+ x 1)))
(+ x y))
在 ((lambda (x y) (+ x y)) 2
(+ x 1))
這裡明顯看到 所以如果你真的想要新變數的值,依賴同一個表達式所設立的另一個變數?在這個情況下,使用一個變形版本 > (let* ((x 1)
(y (+ x 1)))
(+ x y))
3
一個 (let ((x 1))
(let ((y (+ x 1)))
(+ x y)))
> (let (x y)
(list x y))
(NIL NIL)
> (destructuring-bind (w (x y) . z) '(a (b c) d e)
(list w x y z))
(A B C (D E))
若給定的樹(第二個實參)沒有與模式匹配(第一個參數)時,會產生錯誤。 5.3 條件 (Conditionals)¶最簡單的條件式是 (when (oddp that)
(format t "Hmm, that's odd.")
(+ that 1))
等同於 (if (oddp that)
(progn
(format t "Hmm, that's odd.")
(+ that 1)))
所有條件式的母體 (從正反兩面看) 是 (defun our-member (obj lst)
(if (atom lst)
nil
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst)))))
也可以定義成: (defun our-member (obj lst)
(cond ((atom lst) nil)
((eql (car lst) obj) lst)
(t (our-member obj (cdr lst)))))
事實上,Common Lisp 實現大概會把 總得來說呢, > (cond (99))
99
則會返回條件式的值。 由於 當你想要把一個數值與一系列的常數比較時,有 (defun month-length (mon)
(case mon
((jan mar may jul aug oct dec) 31)
((apr jun sept nov) 30)
(feb (if (leap-year) 29 28))
(otherwise "unknown month")))
一個 預設子句的鍵值可以是 > (case 99 (99))
NIL
則
5.4 迭代 (Iteration)¶最基本的迭代運算子是 2.13 節提到 (variable initial update)
在 23 頁的例子中(譯註: 2.13 節), (defun show-squares (start end)
(do ((i start (+ i 1)))
((> i end) 'done)
(format t "~A ~A~%" i (* i i))))
當同時更新超過一個變數時,問題來了,如果一個 > (let ((x 'a))
(do ((x 1 (+ x 1))
(y x x))
((> x 5))
(format t "(~A ~A) " x y)))
(1 A) (2 1) (3 2) (4 3) (5 4)
NIL
每一次迭代時, 但也有一個 > (do* ((x 1 (+ x 1))
(y x x))
((> x 5))
(format t "(~A ~A) " x y))
(1 1) (2 2) (3 3) (4 4) (5 5)
NIL
除了 > (dolist (x '(a b c d) 'done)
(format t "~A " x))
A B C D
DONE
當迭代結束時,初始列表內的第三個表達式 (譯註: 有著同樣的精神的是 (dotimes (x 5 x)
(format t "~A " x))
0 1 2 3 4
5
(譯註:第三個表達式即上例之 Note do 的重點 (THE POINT OF do) 在 “The Evolution of Lisp” 裡,Steele 與 Garbriel 陳述了 do 的重點, 表達的實在太好了,值得整個在這裡引用過來: 撇開爭論語法不談,有件事要說明的是,在任何一個編程語言中,一個迴圈若一次只能更新一個變數是毫無用處的。 幾乎在任何情況下,會有一個變數用來產生下個值,而另一個變數用來累積結果。如果迴圈語法只能產生變數, 那麼累積結果就得藉由賦值語句來“手動”實現…或有其他的副作用。具有多變數的 do 迴圈,體現了產生與累積的本質對稱性,允許可以無副作用地表達迭代過程: (defun factorial (n)
(do ((j n (- j 1))
(f 1 (* j f)))
((= j 0) f)))
當然在 step 形式裡實現所有的實際工作,一個沒有主體的 do 迴圈形式是較不尋常的。 函數 > (mapc #'(lambda (x y)
(format t "~A ~A " x y))
'(hip flip slip)
'(hop flop slop))
HIP HOP FLIP FLOP SLIP SLOP
(HIP FLIP SLIP)
總是回傳 5.5 多值 (Multiple Values)¶曾有人這麼說,爲了要強調函數式編程的重要性,每個 Lisp 表達式都返回一個值。現在事情不是這麼簡單了;在 Common Lisp 裡,一個表達式可以返回零個或多個數值。最多可以返回幾個值取決於各家實現,但至少可以返回 19 個值。 多值允許一個函數返回多件事情的計算結果,而不用構造一個特定的結構。舉例來說,內建的 多值也使得查詢函數可以分辨出
> (values 'a nil (+ 2 4))
A
NIL
6
如果一個 > ((lambda () ((lambda () (values 1 2)))))
1
2
然而若只預期一個返回值時,第一個之外的值會被捨棄: > (let ((x (values 1 2)))
x)
1
通過不帶實參使用 > (values)
> (let ((x (values)))
x)
NIL
要接收多個數值,我們使用 > (multiple-value-bind (x y z) (values 1 2 3)
(list x y z))
(1 2 3)
> (multiple-value-bind (x y z) (values 1 2)
(list x y z))
(1 2 NIL)
如果變數的數量大於數值的數量,剩餘的變數會是 > (multiple-value-bind (s m h) (get-decoded-time)
(format t "~A:~A:~A" h m s))
"4:32:13"
你可以藉由 > (multiple-value-call #'+ (values 1 2 3))
6
還有一個函數是 > (multiple-value-list (values 'a 'b 'c))
(A B C)
看起來像是使用 5.6 中止 (Aborts)¶你可以使用 (defun super ()
(catch 'abort
(sub)
(format t "We'll never see this.")))
(defun sub ()
(throw 'abort 99))
表達式依序求值,就像它們是在 > (super)
99
一個帶有給定標籤的 呼叫 > (progn
(error "Oops!")
(format t "After the error."))
Error: Oops!
Options: :abort, :backtrace
>>
譯註:2 個 關於錯誤與狀態的更多訊息,參見 14.6 小節以及附錄 A。 有時候你想要防止程式被 > (setf x 1)
1
> (catch 'abort
(unwind-protect
(throw 'abort 99)
(setf x 2)))
99
> x
2
在這裡,即便 5.7 範例:日期運算 (Example: Date Arithmetic)¶在某些應用裡,能夠做日期的加減是很有用的 ── 舉例來說,能夠算出從 1997 年 12 月 17 日,六十天之後是 1998 年 2 月 15 日。在這個小節裡,我們會編寫一個實用的工具來做日期運算。我們會將日期轉成整數,起始點設置在 2000 年 1 月 1 日。我們會使用內建的 要將日期轉成數字,我們需要從日期的單位中,算出總天數有多少。舉例來說,2004 年 11 月 13 日的天數總和,是從起始點至 2004 年有多少天,加上從 2004 年到 2004 年 11 月有多少天,再加上 13 天。 有一個我們會需要的東西是,一張列出非潤年每月份有多少天的表格。我們可以使用 Lisp 來推敲出這個表格的內容。我們從列出每月份的長度開始: > (setf mon '(31 28 31 30 31 30 31 31 30 31 30 31))
(31 28 31 30 31 30 31 31 30 31 30 31)
我們可以通過應用 > (apply #'+ mon)
365
現在如果我們反轉這個列表並使用 > (setf nom (reverse mon))
(31 30 31 30 31 31 30 31 30 31 28 31)
> (setf sums (maplist #'(lambda (x)
(apply #'+ x))
nom))
(365 334 304 273 243 212 181 151 120 90 59 31)
這些數字體現了從二月一號開始已經過了 31 天,從三月一號開始已經過了 59 天……等等。 我們剛剛建立的這個列表,可以轉換成一個向量,見圖 5.1,轉換日期至整數的程式。 (defconstant month
#(0 31 59 90 120 151 181 212 243 273 304 334 365))
(defconstant yzero 2000)
(defun leap? (y)
(and (zerop (mod y 4))
(or (zerop (mod y 400))
(not (zerop (mod y 100))))))
(defun date->num (d m y)
(+ (- d 1) (month-num m y) (year-num y)))
(defun month-num (m y)
(+ (svref month (- m 1))
(if (and (> m 2) (leap? y)) 1 0)))
(defun year-num (y)
(let ((d 0))
(if (>= y yzero)
(dotimes (i (- y yzero) d)
(incf d (year-days (+ yzero i))))
(dotimes (i (- yzero y) (- d))
(incf d (year-days (+ y i)))))))
(defun year-days (y) (if (leap? y) 366 365))
圖 5.1 日期運算:轉換日期至數字 典型 Lisp 程式的生命週期有四個階段:先寫好,然後讀入,接著編譯,最後執行。有件 Lisp 非常獨特的事情之一是,在這四個階段時, Lisp 一直都在那裡。可以在你的程式編譯 (參見 10.2 小節)或讀入時 (參見 14.3 小節) 來呼叫 Lisp。我們推導出 效率通常只跟第四個階段有關係,運行期(run-time)。在前三個階段,你可以隨意的使用列表擁有的威力與靈活性,而不需要擔心效率。 若你使用圖 5.1 的程式來造一個時光機器(time machine),當你抵達時,人們大概會不同意你的日期。即使是相對近的現在,歐洲的日期也曾有過偏移,因爲人們會獲得更精準的每年有多長的概念。在說英語的國家,最後一次的不連續性出現在 1752 年,日期從 9 月 2 日跳到 9 月 14 日。 每年有幾天取決於該年是否是潤年。如果該年可以被四整除,我們說該年是潤年,除非該年可以被 100 整除,則該年非潤年 ── 而要是它可以被 400 整除,則又是潤年。所以 1904 年是潤年,1900 年不是,而 1600 年是。 要決定某個數是否可以被另個數整除,我們使用函數 > (mod 23 5)
3
> (mod 25 5)
0
如果第一個實參除以第二個實參的餘數爲 0,則第一個實參是可以被第二個實參整除的。函數 > (mapcar #'leap? '(1904 1900 1600))
(T NIL T)
我們用來轉換日期至整數的函數是 要找到從某年開始的天數和, (defun num->date (n)
(multiple-value-bind (y left) (num-year n)
(multiple-value-bind (m d) (num-month left y)
(values d m y))))
(defun num-year (n)
(if (< n 0)
(do* ((y (- yzero 1) (- y 1))
(d (- (year-days y)) (- d (year-days y))))
((<= d n) (values y (- n d))))
(do* ((y yzero (+ y 1))
(prev 0 d)
(d (year-days y) (+ d (year-days y))))
((> d n) (values y (- n prev))))))
(defun num-month (n y)
(if (leap? y)
(cond ((= n 59) (values 2 29))
((> n 59) (nmon (- n 1)))
(t (nmon n)))
(nmon n)))
(defun nmon (n)
(let ((m (position n month :test #'<)))
(values m (+ 1 (- n (svref month (- m 1)))))))
(defun date+ (d m y n)
(num->date (+ (date->num d m y) n)))
圖 5.2 日期運算:轉換數字至日期 圖 5.2 展示了程式的下半部份。函數 和 函數 圖 5.2 的前兩個函數可以合而爲一。與其返回數值給另一個函數, 有了 > (multiple-value-list (date+ 17 12 1997 60))
(15 2 1998)
我們得到,1998 年 2 月 15 日。 Chapter 5 總結 (Summary)¶
Chapter 5 練習 (Exercises)¶
(a) (let ((x (car y)))
(cons x x))
(b) (let* ((w (car x))
(y (+ w z)))
(cons w y))
> (precedes #\a "abracadabra")
(#\c #\d #\r)
> (intersperse '- '(a b c d))
(A - B - C - D)
(a) 遞迴
(b) do
(c) mapc 與 return
(a) 使用 catch 與 throw 來變更程式,使其找到第一個完整路徑時,直接返回它。
(b) 重寫一個做到同樣事情的程式,但不使用 catch 與 throw。
第六章:函數¶理解函數是理解 Lisp 的關鍵之一。概念上來說,函數是 Lisp 的核心所在。實際上呢,函數是你手邊最有用的工具之一。 6.1 全局函數 (Global Functions)¶謂詞 > (fboundp '+)
T
> (symbol-function '+)
#<Compiled-function + 17BA4E>
可通過 (setf (symbol-function 'add2)
#'(lambda (x) (+ x 2)))
新的全局函數可以這樣定義,用起來和 > (add2 1)
3
實際上 (defun add2 (x) (+ x 2))
翻譯成上述的 通過把 (defun primo (lst) (car lst))
(defun (setf primo) (val lst)
(setf (car lst) val))
在函數名是這種形式 現在任何 > (let ((x (list 'a 'b 'c)))
(setf (primo x) 480)
x)
(480 b c)
不需要爲了定義 由於字串是 Lisp 表達式,沒有理由它們不能出現在程式碼的主體。字串本身是沒有副作用的,除非它是最後一個表達式,否則不會造成任何差別。如果讓字串成爲 (defun foo (x)
"Implements an enhanced paradigm of diversity"
x)
那麼這個字串會變成函數的文檔字串(documentation string)。要取得函數的文檔字串,可以通過呼叫 > (documentation 'foo 'function)
"Implements an enhanced paradigm of diversity"
6.2 區域函數 (Local Functions)¶通過 區域函數可以使用 (name parameters . body)
而 > (labels ((add10 (x) (+ x 10))
(consa (x) (cons 'a x)))
(consa (add10 3)))
(A . 13)
> (labels ((len (lst)
(if (null lst)
0
(+ (len (cdr lst)) 1))))
(len '(a b c)))
3
5.2 節展示了 (do ((x a (b x))
(y c (d y)))
((test x y) (z x y))
(f x y))
等同於 (labels ((rec (x y)
(cond ((test x y)
(z x y))
(t
(f x y)
(rec (b x) (d y))))))
(rec a c))
這個模型可以用來解決,任何你仍然對於 6.3 參數列表 (Parameter Lists)¶2.1 節我們示範過,有了前序表達式, 如果我們在函數的形參列表裡的最後一個變數前,插入 (defun our-funcall (fn &rest args)
(apply fn args))
我們也看過運算子中,有的參數可以被忽略,並可以預設設成特定的值。這樣的參數稱爲選擇性參數(optional parameters)。(相比之下,普通的參數有時稱爲必要參數「required parameters」) 如果符號 (defun pilosoph (thing &optional property)
(list thing 'is property))
那麼在 > (philosoph 'death)
(DEATH IS NIL)
我們可以明確指定預設值,通過將預設值附在列表裡給入。這版的 (defun philosoph (thing &optional (property 'fun))
(list thing 'is property))
有著更鼓舞人心的預設值: > (philosoph 'death)
(DEATH IS FUN)
選擇性參數的預設值可以不是常數。可以是任何的 Lisp 表達式。若這個表達式不是常數,它會在每次需要用到預設值時被重新求值。 一個關鍵字參數(keyword parameter)是一種更靈活的選擇性參數。如果你把符號 > (defun keylist (a &key x y z)
(list a x y z))
KEYLIST
> (keylist 1 :y 2)
(1 NIL 2 NIL)
> (keylist 1 :y 3 :x 2)
(1 2 3 NIL)
和普通的選擇性參數一樣,關鍵字參數預設值爲 關鍵字與其相關的參數可以被剩餘參數收集起來,並傳遞給其他期望收到這些參數的函數。舉例來說,我們可以這樣定義 (defun our-adjoin (obj lst &rest args)
(if (apply #'member obj lst args)
lst
(cons obj lst)))
由於 5.2 節介紹過 (destructuring-bind ((&key w x) &rest y) '((:w 3) a)
(list w x y))
(3 NIL (A))
6.4 範例:實用函數 (Example: Utilities)¶2.6 節提到過,Lisp 大部分是由 Lisp 函陣列成,這些函數與你可以自己定義的函數一樣。這是程式語言中一個有用的特色:你不需要改變你的想法來配合語言,因爲你可以改變語言來配合你的想法。如果你想要 Common Lisp 有某個特定的函數,自己寫一個,而這個函數會成爲語言的一部分,就跟內建的 有經驗的 Lisp 程式設計師,由上而下(top-down)也由下而上 (bottom-up)地工作。當他們朝著語言撰寫程式的同時,也打造了一個更適合他們程式的語言。通過這種方式,語言與程式結合的更好,也更好用。 寫來擴展 Lisp 的運算子稱爲實用函數(utilities)。當你寫了更多 Lisp 程式時,會發現你開發了一系列的程式,而在一個項目寫過許多的實用函數,下個項目裡也會派上用場。 專業的程式設計師常發現,手邊正在寫的程式,與過去所寫的程式有很大的關聯。這就是軟體重用讓人聽起來很吸引人的原因。但重用已經被聯想成物件導向程式設計。但軟體不需要是面向物件的才能重用 ── 這是很明顯的,我們看看程式語言(換言之,編譯器),是重用性最高的軟體。 要獲得可重用軟體的方法是,由下而上地寫程式,而程式不需要是面向物件的才能夠由下而上地寫出。實際上,函數式風格相比之下,更適合寫出重用軟體。想想看 (defun single? (lst)
(and (consp lst) (null (cdr lst))))
(defun append1 (lst obj)
(append lst (list obj)))
(defun map-int (fn n)
(let ((acc nil))
(dotimes (i n)
(push (funcall fn i) acc))
(nreverse acc)))
(defun filter (fn lst)
(let ((acc nil))
(dolist (x lst)
(let ((val (funcall fn x)))
(if val (push val acc))))
(nreverse acc)))
(defun most (fn lst)
(if (null lst)
(values nil nil)
(let* ((wins (car lst))
(max (funcall fn wins)))
(dolist (obj (cdr lst))
(let ((score (funcall fn obj)))
(when (> score max)
(setf wins obj
max score))))
(values wins max))))
圖 6.1 實用函數 你可以通過撰寫實用函數,在程式裡做到同樣的事情。圖 6.1 挑選了一組實用的函數。前兩個 > (single? '(a))
T
而後一個函數 > (append1 '(a b c) 'd)
(A B C D)
下個實用函數是 這在測試的時候非常好用(一個 Lisp 的優點之一是,互動環境讓你可以輕鬆地寫出測試)。如果我們只想要一個 > (map-int #'identity 10)
(0 1 2 3 4 5 6 7 8 9)
然而要是我們想要一個具有 10 個隨機數的列表,每個數介於 0 至 99 之間(包含 99),我們可以忽略參數並只要: > (map-int #'(lambda (x) (random 100))
10)
(85 50 73 64 28 21 40 67 5 32)
我們在 > (filter #'(lambda (x)
(and (evenp x) (+ x 10)))
'(1 2 3 4 5 6 7))
(12 14 16)
另一種思考 圖 6.1 的最後一個函數, > (most #'length '((a b) (a b c) (a)))
(A B C)
3
如果平手的話,返回先馳得點的元素。 注意圖 6.1 的最後三個函數,它們全接受函數作爲參數。 Lisp 使得將函數作爲參數傳遞變得便捷,而這也是爲什麼,Lisp 適合由下而上程式設計的原因之一。成功的實用函數必須是通用的,當你可以將細節作爲函數參數傳遞時,要將通用的部份抽象起來就變得容易許多。 本節給出的函數是通用的實用函數。可以用在任何種類的程式。但也可以替特定種類的程式撰寫實用函數。確實,當我們談到宏時,你可以凌駕於 Lisp 之上,寫出自己的特定語言,如果你想這麼做的話。如果你想要寫可重用軟體,看起來這是最靠譜的方式。 6.5 閉包 (Closures)¶函數可以如表達式的值,或是其它物件那樣被返回。以下是接受一個實參,並依其型別返回特定的結合函數: (defun combiner (x)
(typecase x
(number #'+)
(list #'append)
(t #'list)))
在這之上,我們可以創建一個通用的結合函數: (defun combine (&rest args)
(apply (combiner (car args))
args))
它接受任何型別的參數,並以適合它們型別的方式結合。(爲了簡化這個例子,我們假定所有的實參,都有著一樣的型別。) > (combine 2 3)
5
> (combine '(a b) '(c d))
(A B C D)
2.10 小節提過詞法變數(lexical variables)只在被定義的上下文內有效。伴隨這個限制而來的是,只要那個上下文還有在使用,它們就保證會是有效的。 如果函數在詞法變數的作用域裡被定義時,函數仍可引用到那個變數,即便函數被作爲一個值返回了,返回至詞法變數被創建的上下文之外。下面我們創建了一個把實參加上 > (setf fn (let ((i 3))
#'(lambda (x) (+ x i))))
#<Interpreted-Function C0A51E>
> (funcall fn 2)
5
當函數引用到外部定義的變數時,這外部定義的變數稱爲自由變數(free variable)。函數引用到自由的詞法變數時,稱之爲閉包(closure)。 [2] 只要函數還存在,變數就必須一起存在。 閉包結合了函數與環境(environment);無論何時,當一個函數引用到周圍詞法環境的某個東西時,閉包就被隱式地創建出來了。這悄悄地發生在像是下面這個函數,是一樣的概念: (defun add-to-list (num lst)
(mapcar #'(lambda (x)
(+ x num))
lst))
這函數接受一個數字及列表,並返回一個列表,列表元素是元素與傳入數字的和。在 lambda 表達式裡的變數 一個更顯著的例子會是函數在被呼叫時,每次都返回不同的閉包。下面這個函數返回一個加法器(adder): (defun make-adder (n)
#'(lambda (x)
(+ x n)))
它接受一個數字,並返回一個將該數字與其參數相加的閉包(函數)。 > (setf add3 (make-adder 3))
#<Interpreted-Function COEBF6>
> (funcall add3 2)
5
> (setf add27 (make-adder 27))
#<Interpreted-Function C0EE4E>
> (funcall add27 2)
29
我們可以產生共享變數的數個閉包。下面我們定義共享一個計數器的兩個函數: (let ((counter 0))
(defun reset ()
(setf counter 0))
(defun stamp ()
(setf counter (+ counter 1))))
這樣的一對函數或許可以用來創建時間戳章(time-stamps)。每次我們呼叫 > (list (stamp) (stamp) (reset) (stamp))
(1 2 0 1)
你可以使用全局計數器來做到同樣的事情,但這樣子使用計數器,可以保護計數器被非預期的引用。 Common Lisp 有一個內建的函數 > (mapcar (complement #'oddp)
'(1 2 3 4 5 6))
(NIL T NIL T NIL T)
有了閉包以後,很容易就可以寫出這樣的函數: (defun our-complement (f)
#'(lambda (&rest args)
(not (apply f args))))
如果你停下來好好想想,會發現這是個非凡的小例子;而這僅是冰山一角。閉包是 Lisp 特有的美妙事物之一。閉包開創了一種在別的語言當中,像是不可思議的程式設計方法。 6.6 範例:函數構造器 (Example: Function Builders)¶Dylan 是 Common Lisp 與 Scheme 的混合物,有著 Pascal 一般的語法。它有著大量返回函數的函數:除了上一節我們所看過的 complement ,Dylan 包含: (defun compose (&rest fns)
(destructuring-bind (fn1 . rest) (reverse fns)
#'(lambda (&rest args)
(reduce #'(lambda (v f) (funcall f v))
rest
:initial-value (apply fn1 args)))))
(defun disjoin (fn &rest fns)
(if (null fns)
fn
(let ((disj (apply #'disjoin fns)))
#'(lambda (&rest args)
(or (apply fn args) (apply disj args))))))
(defun conjoin (fn &rest fns)
(if (null fns)
fn
(let ((conj (apply #'conjoin fns)))
#'(lambda (&rest args)
(and (apply fn args) (apply conj args))))))
(defun curry (fn &rest args)
#'(lambda (&rest args2)
(apply fn (append args args2))))
(defun rcurry (fn &rest args)
#'(lambda (&rest args2)
(apply fn (append args2 args))))
(defun always (x) #'(lambda (&rest args) x))
圖 6.2 Dylan 函數建構器 首先, (compose #'a #'b #'c)
返回一個函數等同於 #'(lambda (&rest args) (a (b (apply #'c args))))
這代表著 下面我們建構了一個函數,先給取參數的平方根,取整後再放回列表裡,接著返回: > (mapcar (compose #'list #'round #'sqrt)
'(4 9 16 25))
((2) (3) (4) (5))
接下來的兩個函數, > (mapcar (disjoin #'integerp #'symbolp)
'(a "a" 2 3))
(T NIL T T)
> (mapcar (conjoin #'integerp #'symbolp)
'(a "a" 2 3))
(NIL NIL NIL T)
若考慮將謂詞定義成集合, cddr = (compose #'cdr #'cdr)
nth = (compose #'car #'nthcdr)
atom = (compose #'not #'consp)
= (rcurry #'typep 'atom)
<= = (disjoin #'< #'=)
listp = (disjoin #'< #'=)
= (rcurry #'typep 'list)
1+ = (curry #'+ 1)
= (rcurry #'+ 1)
1- = (rcurry #'- 1)
mapcan = (compose (curry #'apply #'nconc) #'mapcar
complement = (curry #'compose #'not)
圖 6.3 某些等價函數 函數 (curry #'+ 3)
(rcurry #'+ 3)
當函數的參數順序重要時,很明顯可以看出 (funcall (curry #'- 3) 2)
1
而當我們 (funcall (rcurry #'- 3) 2)
-1
最後, 6.7 動態作用域 (Dynamic Scope)¶2.11 小節解釋過區域與全局變數的差別。實際的差別是詞法作用域(lexical scope)的詞法變數(lexical variable),與動態作用域(dynamic scope)的特別變數(special variable)的區別。但這倆幾乎是沒有區別,因爲區域變數幾乎總是是詞法變數,而全局變數總是是特別變數。 在詞法作用域下,一個符號引用到上下文中符號名字出現的地方。區域變數預設有著詞法作用域。所以如果我們在一個環境裡定義一個函數,其中有一個變數叫做 (let ((x 10))
(defun foo ()
x))
則無論 > (let ((x 20)) (foo))
10
而動態作用域,我們在環境中函數被呼叫的地方尋找變數。要使一個變數是動態作用域的,我們需要在任何它出現的上下文中宣告它是 (let ((x 10))
(defun foo ()
(declare (special x))
x))
則函數內的 > (let ((x 20))
(declare (special x))
(foo))
20
新的變數被創建出來之後, 一個 通過在頂層呼叫 > (setf x 30)
30
> (foo)
30
在一個檔案裡的程式碼,如果你不想依賴隱式的特殊宣告,可以使用 動態作用域什麼時候會派上用場呢?通常用來暫時給某個全局變數賦新值。舉例來說,有 11 個變數來控制物件印出的方式,包括了 > (let ((*print-base* 16))
(princ 32))
20
32
這裡顯示了兩件事情,由 6.8 編譯 (Compilation)¶Common Lisp 函數可以獨立被編譯或挨個檔案編譯。如果你只是在頂層輸入一個 > (defun foo (x) (+ x 1))
FOO
許多實現會創建一個直譯的函數(interpreted function)。你可以將函數傳給 > (compiled-function-p #'foo)
NIL
若你將 > (compile 'foo)
FOO
則這個函數會被編譯,而直譯的定義會被編譯出來的取代。編譯與直譯函數的行爲一樣,只不過對 你可以把列表作爲參數傳給 有一種函數你不能作爲參數傳給 通常要編譯 Lisp 程式不是挨個函數編譯,而是使用 當一個函數包含在另一個函數內時,包含它的函數會被編譯,而且內部的函數也會被編譯。所以 > (compile 'make-adder)
MAKE-ADDER
> (compiled-function-p (make-adder 2))
T
6.9 使用遞迴 (Using Recursion)¶比起多數別的語言,遞迴在 Lisp 中扮演了一個重要的角色。這主要有三個原因:
學生們起初會覺得遞迴很難理解。但 3.9 節指出了,如果你想要知道是否正確,不需要去想遞迴函數所有的呼叫過程。 同樣的如果你想寫一個遞迴函數。如果你可以描述問題是怎麼遞迴解決的,通常很容易將解法轉成程式。要使用遞迴來解決一個問題,你需要做兩件事:
如果這兩件事完成了,那問題就解決了。因爲遞迴每次都將問題變得更小,而一個有限的問題終究會被解決的,而最小的問題僅需幾個有限的步驟就能解決。 舉例來說,下面這個找到一個正規列表(proper list)長度的遞迴算法,我們每次遞迴時,都可以找到更小列表的長度:
當這個描述翻譯成程式時,先處理基本用例;但公式化遞迴演算法時,我們通常從一般情況下手。 前述的演算法,明確地描述了一種找到正規列表長度的方法。當你定義一個遞迴函數時,你必須要確定你在分解問題時,問題實際上越變越小。取得一個正規列表的 這裡有兩個遞迴算法的範例。假定參數是有限的。注意第二個範例,我們每次遞迴時,將問題分成兩個更小的問題: 第一個例子, 第二個例子, 一旦你可以這樣描述算法,要寫出遞迴函數只差一步之遙。 某些算法通常是這樣表達最自然,而某些算法不是。你可能需要翻回前面,試試不使用遞迴來定義 如果你關心效率,有兩個你需要考慮的議題。第一,尾遞迴(tail-recursive),會在 13.2 節討論。一個好的編譯器,使用迴圈或是尾遞迴的速度,應該是沒有或是區別很小的。然而如果你需要使函數變成尾遞迴的形式時,或許直接用迭代會更好。 另一個需要銘記在心的議題是,最顯而易見的遞迴算法,不一定是最有效的。經典的例子是費氏函數。它是這樣遞迴地被定義的,
直接翻譯這個定義, (defun fib (n)
(if (<= n 1)
1
(+ (fib (- n 1))
(fib (- n 2)))))
這樣是效率極差的。一次又一次的重複計算。如果你要找 下面是一個算出同樣結果的迭代版本: (defun fib (n)
(do ((i n (- i 1))
(f1 1 (+ f1 f2))
(f2 1 f1))
((<= i 1) f1)))
迭代的版本不如遞迴版本來得直觀,但是效率遠遠高出許多。這樣的事情在實踐中常發生嗎?非常少 ── 這也是爲什麼所有的教科書都使用一樣的例子 ── 但這是需要注意的事。 Chapter 6 總結 (Summary)¶
Chapter 6 練習 (Exercises)¶
腳註
第七章:輸入與輸出¶Common Lisp 有著威力強大的 I/O 工具。針對輸入以及一些普遍讀取字元的函數,我們有 Common Lisp 有兩種流 (streams),字元流與二進制流。本章描述了字元流的操作;二進制流的操作涵蓋在 14.2 節。 7.1 流 (Streams)¶流是用來表示字元來源或終點的 Lisp 物件。要從檔案讀取或寫入,你將檔案作爲流打開。但流與檔案是不一樣的。當你在頂層讀入或印出時,你也可以使用流。你甚至可以創建可以讀取或寫入字串的流。 輸入預設是從 我們已經看過 路徑名(pathname)是一種指定一個檔案的可移植方式。路徑名包含了六個部分:host、device、directory、name、type 及 version。你可以通過呼叫 > (setf path (make-pathname :name "myfile"))
#P"myfile"
開啓一個檔案的基本函數是 你可以在創建流時,指定你想要怎麼使用它。 無論你是要寫入流、從流讀取或是兩者皆是, > (setf str (open path :direction :output
:if-exists :supersede))
#<Stream C017E6>
流的列印表示法因實現而異 (implementation-dependent)。 現在我們可以把這個流作爲第一個參數傳給 > (format str "Something~%")
NIL
如果我們在此時檢視這個檔案,輸出也許會、也許不會在那裡。某些實現會將輸出儲存成一塊 (chunks)再寫出。它也許不會出現,直到我們將流關閉: > (close str)
NIL
當你使用完時,永遠記得關閉檔案;在你還沒關閉之前,內容是不保證會出現的。現在如果我們檢視檔案 “myfile” ,應該有一行: Something
如果我們只想從一個檔案讀取,我們可以開啓一個具有 > (setf str (open path :direction :input))
#<Stream C01C86>
我們可以對一個檔案使用任何輸入函數。7.2 節會更詳細的描述輸入。這裡作爲一個範例,我們將使用 > (read-line str)
"Something"
> (close str)
NIL
當你讀取完畢時,記得關閉檔案。 大部分時間我們不使用 (with-open-file (str path :direction :output
:if-exists :supersede)
(format str "Something~%"))
7.2 輸入 (Input)¶兩個最受歡迎的輸入函數是 > (progn
(format t "Please enter your name: ")
(read-line))
Please enter your name: Rodrigo de Bivar
"Rodrigo de Bivar"
NIL
譯註:Rodrigo de Bivar 人稱熙德 (El cid),十一世紀的西班牙民族英雄。 如果你想要原封不動的輸出,這是你該用的函數。(第二個返回值只在 在一般情況下, 所以要在頂層顯示一個檔案的內容,我們可以使用下面這個函數: (defun pseudo-cat (file)
(with-open-file (str file :direction :input)
(do ((line (read-line str nil 'eof)
(read-line str nil 'eof)))
((eql line 'eof))
(format t "~A~%" line))))
如果我們想要把輸入解析爲 Lisp 物件,使用 如果我們在頂層使用 > (read)
(a
b
c)
(A B C)
換句話說,如果我們在一行裡面輸入許多表達式, > (ask-number)
Please enter a number. a b
Please enter a number. Please enter a number. 43
43
兩個連續的提示符 (successive prompts)在第二行被印出。第一個 你或許想要避免使用 > (read-from-string "a b c")
A
2
它同時返回第二個值,一個指出停止讀取字串時的位置的數字。 在一般情況下, 所有的這些輸入函數是由基本函數 (primitive) 7.3 輸出 (Output)¶三個最簡單的輸出函數是
> (prin1 "Hello")
"Hello"
"Hello"
> (princ "Hello")
Hello
"Hello"
兩者皆返回它們的第一個參數 (譯註: 第二個值是返回值) ── 順道一提,是用 有這些函數的背景知識在解釋更爲通用的 如果我們把 由於每人的觀點不同, > (format nil "Dear ~A, ~% Our records indicate..."
"Mr. Malatesta")
"Dear Mr. Malatesta,
Our records indicate..."
這裡
> (format t "~S ~A" "z" "z")
"z" z
NIL
格式化指令可以接受參數。
下面是一個有五個參數的罕見例子: ? (format nil "~10,2,0,'*,' F" 26.21875)
" 26.22"
這是原本的數字取至小數點第二位、(小數點向左移 0 位)、在 10 個字元的空間裡向右對齊,左邊補滿空白。注意作爲參數給入是寫成 所有的這些參數都是選擇性的。要使用預設值你可以直接忽略對應的參數。如果我們想要做的是,印出一個小數點取至第二位的數字,我們可以說: > (format nil "~,2,,,F" 26.21875)
"26.22"
你也可以忽略一系列的尾隨逗號 (trailing commas),前面指令更常見的寫法會是: > (format nil "~,2F" 26.21875)
"26.22"
警告: 當 7.4 範例:字串代換 (Example: String Substitution)¶作爲一個 I/O 的範例,本節示範如何寫一個簡單的程式來對文字檔案做字串替換。我們即將寫一個可以將一個檔案中,舊的字串 而要是 一個暫時儲存輸入的佇列 (queue)稱作緩衝區 (buffer)。在這個情況裡,因爲我們知道我們不需要儲存超過一個預定的字元量,我們可以使用一個叫做環狀緩衝區 在圖 7.1 的程式裡,實現了環狀緩衝區的操作。 另外兩個索引, start ≤ used ≤ new ≤ end
你可以把 函數 要插入一個新值至緩衝區,我們將使用 (defstruct buf
vec (start -1) (used -1) (new -1) (end -1))
(defun bref (buf n)
(svref (buf-vec buf)
(mod n (length (buf-vec buf)))))
(defun (setf bref) (val buf n)
(setf (svref (buf-vec buf)
(mod n (length (buf-vec buf))))
val))
(defun new-buf (len)
(make-buf :vec (make-array len)))
(defun buf-insert (x b)
(setf (bref b (incf (buf-end b))) x))
(defun buf-pop (b)
(prog1
(bref b (incf (buf-start b)))
(setf (buf-used b) (buf-start b)
(buf-new b) (buf-end b))))
(defun buf-next (b)
(when (< (buf-used b) (buf-new b))
(bref b (incf (buf-used b)))))
(defun buf-reset (b)
(setf (buf-used b) (buf-start b)
(buf-new b) (buf-end b)))
(defun buf-clear (b)
(setf (buf-start b) -1 (buf-used b) -1
(buf-new b) -1 (buf-end b) -1))
(defun buf-flush (b str)
(do ((i (1+ (buf-used b)) (1+ i)))
((> i (buf-end b)))
(princ (bref b i) str)))
圖 7.1 環狀緩衝區的操作 接下來我們需要兩個特別爲這個應用所寫的函數: 最後 在圖 7.1 定義的函數被圖 7.2 所使用,包含了字串替換的程式。函數 第二個函數 變數 (defun file-subst (old new file1 file2)
(with-open-file (in file1 :direction :input)
(with-open-file (out file2 :direction :output
:if-exists :supersede)
(stream-subst old new in out))))
(defun stream-subst (old new in out)
(let* ((pos 0)
(len (length old))
(buf (new-buf len))
(from-buf nil))
(do ((c (read-char in nil :eof)
(or (setf from-buf (buf-next buf))
(read-char in nil :eof))))
((eql c :eof))
(cond ((char= c (char old pos))
(incf pos)
(cond ((= pos len) ; 3
(princ new out)
(setf pos 0)
(buf-clear buf))
((not from-buf) ; 2
(buf-insert c buf))))
((zerop pos) ; 1
(princ c out)
(when from-buf
(buf-pop buf)
(buf-reset buf)))
(t ; 4
(unless from-buf
(buf-insert c buf))
(princ (buf-pop buf) out)
(buf-reset buf)
(setf pos 0))))
(buf-flush buf out)))
圖 7.2 字串替換 下列表格展示了當我們將檔案中的
第一欄是當前字元 ── 在檔案 The struggle between Liberty and Authority is the most conspicuous feature
in the portions of history with which we are earliest familiar, particularly
in that of Greece, Rome, and England.
在我們對 The struggle between Liberty and Authority is ze most conspicuous feature
in ze portions of history with which we are earliest familiar, particularly
in zat of Greece, Rome, and England.
爲了使這個例子儘可能的簡單,圖 7.2 的程式只將一個字串換成另一個字串。很容易擴展爲搜索一個模式而不是一個字面字串。你只需要做的是,將 7.5 宏字元 (Macro Characters)¶一個宏字元 (macro character)是獲得 一個宏字元或宏字元組合也稱作 > (car (read-from-string "'a"))
QUOTE
引用對於讀取宏來說是不尋常的,因爲它用單一字元表示。有了一個有限的字元集,你可以在 Common Lisp 裡有許多單一字元的讀取宏,來表示一個或更多字元。 這樣的讀取宏叫做派發 (dispatching)讀取宏,而第一個字元叫做派發字元 (dispatching character)。所有預定義的派發讀取宏使用井號 ( 其它我們見過的派發讀取宏包括 > (let ((*print-array* t))
(vectorp (read-from-string (format nil "~S"
(vector 1 2)))))
T
當然我們拿回來的不是同一個向量,而是具有同樣元素的新向量。 不是所有物件被顯示時都有著清楚 (distinct)、可讀的形式。舉例來說,函數與雜湊表,傾向於這樣 當你定義你自己的事物表示法時 (舉例來說,結構的印出函數),你要將此準則記住。要不使用一個可以被讀回來的表示法,或是使用 Chapter 7 總結 (Summary)¶
Chapter 7 練習 (Exercises)¶
腳註
第八章:符號¶我們一直在使用符號。在符號看似簡單的表面之下,又好像沒有那麼簡單。起初最好不要糾結於背後的實現機制。可以把符號當成資料物件與名字那樣使用,而不需要理解兩者是如何關聯起來的。但到了某個時間點,停下來思考背後是究竟是如何工作會是很有用的。本章解釋了背後實現的細節。 8.1 符號名 (Symbol Names)¶第二章描述過,符號是變數的名字,符號本身以物件所存在。但 Lisp 符號的可能性,要比在多數語言僅允許作爲變數名來得廣泛許多。實際上,符號可以用任何字串當作名字。可以通過呼叫 > (symbol-name 'abc)
"ABC"
注意到這個符號的名字,打印出來都是大寫字母。預設情況下, Common Lisp 在讀入時,會把符號名字所有的英文字母都轉成大寫。代表 Common Lisp 預設是不分大小寫的: > (eql 'abc 'Abc)
T
> (CaR '(a b c))
A
一個名字包含空白,或其它可能被讀取器認爲是重要的字元的符號,要用特殊的語法來引用。任何存在垂直槓 (vertical bar)之間的字元序列將被視爲符號。可以如下這般在符號的名字中,放入任何字元: > (list '|Lisp 1.5| '|| '|abc| '|ABC|)
(|Lisp 1.5| || |abc| ABC)
當這種符號被讀入時,不會有大小寫轉換,而宏字元與其他的字元被視爲一般字元。 那什麼樣的符號不需要使用垂直槓來參照呢?基本上任何不是數字,或不包含讀取器視爲重要的字元的符號。一個快速找出你是否可以不用垂直槓來引用符號的方法,是看看 Lisp 如何印出它的。如果 Lisp 沒有用垂直槓表示一個符號,如上述列表的最後一個,那麼你也可以不用垂直槓。 記得,垂直槓是一種表示符號的特殊語法。它們不是符號的名字之一: > (symbol-name '|a b c|)
"a b c"
(如果想要在符號名稱內使用垂直槓,可以放一個反斜線在垂直槓的前面。) 譯註: 反斜線是 8.2 屬性列表 (Property Lists)¶在 Common Lisp 裡,每個符號都有一個屬性列表(property-list)或稱爲 > (get 'alizarin 'color)
NIL
它使用 要將值與鍵關聯起來時,你可以使用 > (setf (get 'alizarin 'color) 'red)
RED
> (get 'alizarin 'color)
RED
現在符號 ![]() 圖 8.1 符號的結構 > (setf (get 'alizarin 'transparency) 'high)
HIGH
> (symbol-plist 'alizarin)
(TRANSPARENCY HIGH COLOR RED)
注意,屬性列表不以關聯列表(assoc-lists)的形式表示,雖然用起來感覺是一樣的。 在 Common Lisp 裡,屬性列表用得不多。他們大部分被雜湊表取代了(4.8 小節)。 8.3 符號很不簡單 (Symbols Are Big)¶當我們輸入名字時,符號就被悄悄地創建出來了,而當它們被顯示時,我們只看的到符號的名字。某些情況下,把符號想成是表面所見的東西就好,別想太多。但有時候符號不像看起來那麼簡單。 從我們如何使用以及檢視符號,符號看起來像是整數那樣的小物件。而符號實際上確實是一個物件,差不多像是由 很少有程式會使用很多符號,以致於值得用其它的東西來代替符號以節省空間。但需要記住的是,符號是實際的物件,不僅是名字而已。當兩個變數設成相同的符號時,與兩個變數設成相同列表一樣:兩個變數的指標都指向同樣的物件。 8.4 創建符號 (Creating Symbols)¶8.1 節示範了如何取得符號的名字。另一方面,用字串生成符號也是有可能的。但比較複雜一點,因爲我們需要先介紹包(package)。 概念上來說,包是將名字映射到符號的符號表(symbol-tables)。每個普通的符號都屬於一個特定的包。符號屬於某個包,我們稱爲符號被包扣押(intern)了。函數與變數用符號作爲名稱。包藉由限制哪個符號可以存取來實現模組性(modularity),也是因爲這樣,我們才可以引用到函數與變數。 大多數的符號在讀取時就被扣押了。在第一次輸入一個新符號的名字時,Lisp 會產生一個新的符號物件,並將它扣押到當下的包裡(預設是 > (intern "RANDOM-SYMBOL")
RANDOM-SYMBOL
NIL
選擇性包參數預設是當前的包,所以前述的表達式,返回當前包裡的一個符號,此符號的名字是 “RANDOM-SYMBOL”,若此符號尚未存在時,會創建一個這樣的符號出來。第二個返回值告訴我們符號是否存在;在這個情況,它不存在。 不是所有的符號都會被扣押。有時候有一個自由的(uninterned)符號是有用的,這和公用電話本是一樣的原因。自由的符號叫做 gensyms 。我們將會在第 10 章討論宏(Macro)時,理解 8.5 多重包 (Multiple Packages)¶大的程式通常切分爲多個包。如果程式的每個部分都是一個包,那麼開發程式另一個部分的某個人,將可以使用符號來作爲函數名或變數名,而不必擔心名字在別的地方已經被用過了。 在沒有提供定義多個命名空間的語言裡,工作於大項目的程式設計師,通常需要想出某些規範(convention),來確保他們不會使用同樣的名稱。舉例來說,程式設計師寫顯示相關的程式(display code)可能用 包不過是提供了一種方便的方式來自動辦到此事。如果你將函數定義在單獨的包裡,可以隨意使用你喜歡的名字。只有你明確導出( 舉例來說,假設一個程式分爲兩個包, 下面是你可能會放在檔案最上方,包含獨立包的程式: (defpackage "MY-APPLICATION"
(:use "COMMON-LISP" "MY-UTILITIES")
(:nicknames "APP")
(:export "WIN" "LOSE" "DRAW"))
(in-package my-application)
8.6 關鍵字 (Keywords)¶在 爲什麼使用關鍵字而不用一般的符號?因爲關鍵字在哪都可以存取。一個函數接受符號作爲實參,應該要寫成預期關鍵字的函數。舉例來說,這個函數可以安全地在任何包裡呼叫: (defun noise (animal)
(case animal
(:dog :woof)
(:cat :meow)
(:pig :oink)))
但如果是用一般符號寫成的話,它只在被定義的包內正常工作,除非關鍵字也被導出了。 8.7 符號與變數 (Symbols and Variables)¶Lisp 有一件可能會使你困惑的事情是,符號與變數的從兩個非常不同的層面互相關聯。當符號是特別變數(special variable)的名字時,變數的值存在符號的 value 欄位(圖 8.1)。 而對於詞法變數(lexical variables)來說,事情就完全不一樣了。一個作爲詞法變數的符號只不過是個佔位符(placeholder)。編譯器會將其轉爲一個寄存器(register)或記憶體位置的引用位址。在最後編譯出來的 在程式裡,我們無法追蹤這個符號 (除非它被保存在除錯器「debugger」的某個地方)。因此符號與詞法變數的值之間是沒有連接的;只要一有值,符號就消失了。 8.8 範例:隨機文字 (Example: Random Text)¶如果你要寫一個處理單詞的程式,通常使用符號會比字串來得好,因爲符號概念上是原子性的(atomic)。符號可以用 產生的文字將會是部分可信的(locally plausible),因爲任兩個出現的單詞也是輸入檔案裡,兩個同時出現的單詞。令人驚訝的是,獲得看起來是 ── 有意義的整句 ── 甚至整個段落是的頻率相當高。 圖 8.2 包含了程式的上半部,用來讀取範例檔案的程式。 (defparameter *words* (make-hash-table :size 10000))
(defconstant maxword 100)
(defun read-text (pathname)
(with-open-file (s pathname :direction :input)
(let ((buffer (make-string maxword))
(pos 0))
(do ((c (read-char s nil :eof)
(read-char s nil :eof)))
((eql c :eof))
(if (or (alpha-char-p c) (char= c #\'))
(progn
(setf (aref buffer pos) c)
(incf pos))
(progn
(unless (zerop pos)
(see (intern (string-downcase
(subseq buffer 0 pos))))
(setf pos 0))
(let ((p (punc c)))
(if p (see p)))))))))
(defun punc (c)
(case c
(#\. '|.|) (#\, '|,|) (#\; '|;|)
(#\! '|!|) (#\? '|?|) ))
(let ((prev `|.|))
(defun see (symb)
(let ((pair (assoc symb (gethash prev *words*))))
(if (null pair)
(push (cons symb 1) (gethash prev *words*))
(incf (cdr pair))))
(setf prev symb)))
圖 8.2 讀取範例檔案 從圖 8.2 所導出的資料,會被存在雜湊表 ((|sin| . 1) (|wide| . 2) (|sights| . 1))
使用彌爾頓的失樂園作爲範例檔案時,這是與鍵 函數 只要下個字元是一個字(由 函數 在 現在來到了有趣的部份。圖 8.3 包含了從圖 8.2 所累積的資料來產生文字的程式。 (defun generate-text (n &optional (prev '|.|))
(if (zerop n)
(terpri)
(let ((next (random-next prev)))
(format t "~A " next)
(generate-text (1- n) next))))
(defun random-next (prev)
(let* ((choices (gethash prev *words*))
(i (random (reduce #'+ choices
:key #'cdr))))
(dolist (pair choices)
(if (minusp (decf i (cdr pair)))
(return (car pair))))))
圖 8.3 產生文字 要取得一個新的單詞, 現在會是測試運行下程式的好時機。但其實你早看過一個它所產生的範例: 就是本書開頭的那首詩,是使用彌爾頓的失樂園作爲輸入檔案所產生的。 (譯註: 詩可在這裡看,或是瀏覽書的第 vi 頁) Half lost on my firmness gains more glad heart, Or violent and from forage drives A glimmering of all sun new begun Both harp thy discourse they match’d, Forth my early, is not without delay; For their soft with whirlwind; and balm. Undoubtedly he scornful turn’d round ninefold, Though doubled now what redounds, And chains these a lower world devote, yet inflicted? Till body or rare, and best things else enjoy’d in heav’n To stand divided light at ev’n and poise their eyes, Or nourish, lik’ning spiritual, I have thou appear. ── Henley Chapter 8 總結 (Summary)¶
Chapter 8 練習 (Exercises)¶
腳註
第九章:數字¶處理數字是 Common Lisp 的強項之一。Common Lisp 有著豐富的數值型別,而 Common Lisp 操作數字的特性與其他語言比起來更受人喜愛。 9.1 型別 (Types)¶Common Lisp 提供了四種不同型別的數字:整數、浮點數、比值與複數。本章所講述的函數適用於所有型別的數字。有幾個不能用在複數的函數會特別說明。 整數寫成一串數字:如 謂詞 ![]() 圖 9.1: 數值型別 要決定計算過程會返回何種數字,以下是某些通用的經驗法則:
第二、第三個規則可以在讀入參數時直接應用,所以: > (list (ratiop 2/2) (complexp #c(1 0)))
(NIL NIL)
9.2 轉換及取出 (Conversion and Extraction)¶Lisp 提供四種不同型別的數字的轉換及取出位數的函數。函數 > (mapcar #'float '(1 2/3 .5))
(1.0 0.6666667 0.5)
將數字轉成整數未必需要轉換,因爲它可能牽涉到某些資訊的喪失。函數 > (truncate 1.3)
1
0.29999995
第二個返回值 函數 (defun palindrome? (x)
(let ((mid (/ (length x) 2)))
(equal (subseq x 0 (floor mid))
(reverse (subseq x (ceiling mid))))))
和 > (floor 1.5)
1
0.5
實際上,我們可以把 (defun our-truncate (n)
(if (> n 0)
(floor n)
(ceiling n)))
函數 > (mapcar #'round '(-2.5 -1.5 1.5 2.5))
(-2 -2 2 2)
在某些數值應用中這是好事,因爲舍入誤差(rounding error)通常會互相抵消。但要是用戶期望你的程式將某些值取整數時,你必須自己提供這個功能。 [1] 與其他的函數一樣, 函數 關於實數,函數 > (mapcar #'signum '(-2 -0.0 0.0 0 .5 3))
(-1 -0.0 0.0 0 1.0 1)
在某些應用裡, 比值與複數概念上是兩部分的結構。(譯註:像 Cons 這樣的兩部分結構) 函數 函數 9.3 比較 (Comparison)¶謂詞 > (= 1 1.0)
T
> (eql 1 1.0)
NIL
用來比較數字的謂詞爲 (<= w x y z)
等同於二元運算子的結合(conjunction),應用至每一對參數上: (and (<= w x) (<= x y) (<= y z))
由於 (/= w x y z)
等同於 (and (/= w x) (/= w y) (/= w z)
(/= x y) (/= y z) (/= y z))
特殊的謂詞 > (list (minusp -0.0) (zerop -0.0))
(NIL T)
因此對 謂詞 本節定義的謂詞中,只有 函數 > (list (max 1 2 3 4 5) (min 1 2 3 4 5))
(5 1)
如果參數含有浮點數的話,結果的型別取決於各家實現。 9.4 算術 (Arithematic)¶用來做加減的函數是 (- x y z)
等同於 (- (- x y) z)
有兩個函數 宏 用來做乘法的函數是 除法函數 > (/ 3)
1/3
而這樣形式的呼叫 (/ x y z)
等同於 (/ (/ x y) z)
注意 當給定兩個整數時, > (/ 365 12)
365/12
舉例來說,如果你試著找出平均每一個月有多長,可能會有頂層在逗你玩的感覺。在這個情況下,你需要的是,對比值呼叫 > (float 365/12)
30.416666
9.5 指數 (Exponentiation)¶要找到 \(x^n\) 呼叫 > (expt 2 5)
32
而要找到 \(log_nx\) 呼叫 > (log 32 2)
5.0
通常返回一個浮點數。 要找到 \(e^x\) 有一個特別的函數 > (exp 2)
7.389056
而要找到自然對數,你可以使用 > (log 7.389056)
2.0
要找到立方根,你可以呼叫 > (expt 27 1/3)
3.0
但要找到平方根,函數 > (sqrt 4)
2.0
9.6 三角函數 (Trigometric Functions)¶常數 > (let ((x (/ pi 4)))
(list (sin x) (cos x) (tan x)))
(0.7071067811865475d0 0.7071067811865476d0 1.0d0)
;;; 譯註: CCL 1.8 SBCL 1.0.55 下的結果是
;;; (0.7071067811865475D0 0.7071067811865476D0 0.9999999999999999D0)
這些函數都接受負數及複數參數。 函數 雙曲正弦、雙曲餘弦及雙曲正交分別由 9.7 表示法 (Representations)¶Common Lisp 沒有限制整數的大小。可以塞進一個字(word)記憶體的小整數稱爲定長數(fixnums)。在計算過程中,整數無法塞入一個字時,Lisp 切換至使用多個字的表示法(一個大數 「bignum」)。所以整數的大小限製取決於實體記憶體,而不是語言。 常數 > (values most-positive-fixnum most-negative-fixnum)
536870911
-536870912
;;; 譯註: CCL 1.8 的結果爲
1152921504606846975
-1152921504606846976
;;; SBCL 1.0.55 的結果爲
4611686018427387903
-4611686018427387904
謂詞 > (typep 1 'fixnum)
T
> (type (1+ most-positive-fixnum) 'bignum)
T
浮點數的數值限制是取決於各家實現的。 Common Lisp 提供了至多四種型別的浮點數:短浮點 一般來說,短浮點應可塞入一個字,單浮點與雙浮點提供普遍的單精度與雙精度浮點數的概念,而長浮點,如果想要的話,可以是很大的數。但實現可以不對這四種型別做區別,也是完全沒有問題的。 你可以指定你想要何種格式的浮點數,當數字是用科學表示法時,可以通過將 (譯註: 在給定的實現裡,用十六個全局常數標明了每個格式的限制。它們的名字是這種形式: 浮點數下溢(underflow)與溢出(overflow),都會被 Common Lisp 視爲錯誤 : > (* most-positive-long-float 10)
Error: floating-point-overflow
9.8 範例:追蹤光線 (Example: Ray-Tracing)¶作爲一個數值應用的範例,本節示範了如何撰寫一個光線追蹤器 (ray-tracer)。光線追蹤是一個高級的 (deluxe)渲染算法: 它產生出逼真的圖像,但需要花點時間。 要產生一個 3D 的圖像,我們至少需要定義四件事: 一個觀測點 (eye)、一個或多個光源、一個由一個或多個平面所組成的模擬世界 (simulated world),以及一個作爲通往這個世界的窗戶的平面 (圖像平面「image plane」)。我們產生出的是模擬世界投影在圖像平面區域的圖像。 光線追蹤獨特的地方在於,我們如何找到這個投影: 我們一個一個像素地沿著圖像平面走,追蹤回到模擬世界裡的光線。這個方法帶來三個主要的優勢: 它讓我們容易得到現實世界的光學效應 (optical effect),如透明度 (transparency)、反射光 (reflected light)以及產生陰影 (cast shadows);它讓我們可以直接用任何我們想要的幾何的物體,來定義出模擬的世界,而不需要用多邊形 (polygons)來建構它們;以及它很簡單實現。 (defun sq (x) (* x x))
(defun mag (x y z)
(sqrt (+ (sq x) (sq y) (sq z))))
(defun unit-vector (x y z)
(let ((d (mag x y z)))
(values (/ x d) (/ y d) (/ z d))))
(defstruct (point (:conc-name nil))
x y z)
(defun distance (p1 p2)
(mag (- (x p1) (x p2))
(- (y p1) (y p2))
(- (z p1) (z p2))))
(defun minroot (a b c)
(if (zerop a)
(/ (- c) b)
(let ((disc (- (sq b) (* 4 a c))))
(unless (minusp disc)
(let ((discrt (sqrt disc)))
(min (/ (+ (- b) discrt) (* 2 a))
(/ (- (- b) discrt) (* 2 a))))))))
圖 9.2 實用數學函數 圖 9.2 包含了我們在光線追蹤器裡會需要用到的一些實用數學函數。第一個 > (multiple-value-call #'mag (unit-vector 23 12 47))
1.0
我們在 最後
\[x = \dfrac{-b \pm \sqrt{b^2 - 4ac}}{2a}\]
圖 9.3 包含了定義一個最小光線追蹤器的程式。 它產生通過單一光源照射的黑白圖像,與觀測點 (eye)處於同個位置。 (結果看起來像是閃光攝影術 (flash photography)拍出來的)
(defstruct surface color)
(defparameter *world* nil)
(defconstant eye (make-point :x 0 :y 0 :z 200))
(defun tracer (pathname &optional (res 1))
(with-open-file (p pathname :direction :output)
(format p "P2 ~A ~A 255" (* res 100) (* res 100))
(let ((inc (/ res)))
(do ((y -50 (+ y inc)))
((< (- 50 y) inc))
(do ((x -50 (+ x inc)))
((< (- 50 x) inc))
(print (color-at x y) p))))))
(defun color-at (x y)
(multiple-value-bind (xr yr zr)
(unit-vector (- x (x eye))
(- y (y eye))
(- 0 (z eye)))
(round (* (sendray eye xr yr zr) 255))))
(defun sendray (pt xr yr zr)
(multiple-value-bind (s int) (first-hit pt xr yr zr)
(if s
(* (lambert s int xr yr zr) (surface-color s))
0)))
(defun first-hit (pt xr yr zr)
(let (surface hit dist)
(dolist (s *world*)
(let ((h (intersect s pt xr yr zr)))
(when h
(let ((d (distance h pt)))
(when (or (null dist) (< d dist))
(setf surface s hit h dist d))))))
(values surface hit)))
(defun lambert (s int xr yr zr)
(multiple-value-bind (xn yn zn) (normal s int)
(max 0 (+ (* xr xn) (* yr yn) (* zr zn)))))
圖 9.3 光線追蹤。 圖像平面會是由 x 軸與 y 軸所定義的平面。觀測者 (eye) 會在 z 軸,距離原點 200 個單位。所以要在圖像平面可以被看到,插入至 ![]() 圖 9.4: 追蹤光線。 函數 圖片的解析度可以通過給入明確的 圖片是一個在圖像平面 100x100 的正方形。每一個像素代表著穿過圖像平面抵達觀測點的光的數量。要找到每個像素光的數量, 要決定一個光線的亮度, 朗伯定律 告訴我們,由平面上一點所反射的光的強度,正比於該點的單位法向量 (unit normal vector) N (這裡是與平面垂直且長度爲一的向量)與該點至光源的單位向量 L 的點積 (dot-product):
\[i = N·L\]
如果光剛好照到這點, N 與 L 會重合 (coincident),則點積會是最大值, 在我們的程式裡,我們假設光源在觀測點 (eye),所以 在 爲了簡單起見,我們在模擬世界裡會只有一種物體,球體。圖 9.5 包含了與球體有關的程式碼。球體結構包含了 (defstruct (sphere (:include surface))
radius center)
(defun defsphere (x y z r c)
(let ((s (make-sphere
:radius r
:center (make-point :x x :y y :z z)
:color c)))
(push s *world*)
s))
(defun intersect (s pt xr yr zr)
(funcall (typecase s (sphere #'sphere-intersect))
s pt xr yr zr))
(defun sphere-intersect (s pt xr yr zr)
(let* ((c (sphere-center s))
(n (minroot (+ (sq xr) (sq yr) (sq zr))
(* 2 (+ (* (- (x pt) (x c)) xr)
(* (- (y pt) (y c)) yr)
(* (- (z pt) (z c)) zr)))
(+ (sq (- (x pt) (x c)))
(sq (- (y pt) (y c)))
(sq (- (z pt) (z c)))
(- (sq (sphere-radius s)))))))
(if n
(make-point :x (+ (x pt) (* n xr))
:y (+ (y pt) (* n yr))
:z (+ (z pt) (* n zr))))))
(defun normal (s pt)
(funcall (typecase s (sphere #'sphere-normal))
s pt))
(defun sphere-normal (s pt)
(let ((c (sphere-center s)))
(unit-vector (- (x c) (x pt))
(- (y c) (y pt))
(- (z c) (z pt)))))
圖 9.5 球體。 函數 我們要怎麼找到一束光與一個球體的交點 (intersection)呢?光線是表示成點 \(p =〈x_0,y_0,x_0〉\) 以及單位向量 \(v =〈x_r,y_r,x_r〉\) 。每個在光上的點可以表示爲 \(p+nv\) ,對於某個 n ── 即 \(〈x_0+nx_r,y_0+ny_r,z_0+nz_r〉\) 。光擊中球體的點的距離至中心 \(〈x_c,y_c,z_c〉\) 會等於球體的半徑 r 。所以在下列這個交點的方程式會成立:
\[r = \sqrt{ (x_0 + nx_r - x_c)^2 + (y_0 + ny_r - y_c)^2 + (z_0 + nz_r - z_c)^2 }\]
這會給出
\[an^2 + bn + c = 0\]
其中
\[\begin{split}a = x_r^2 + y_r^2 + z_r^2\\b = 2((x_0-x_c)x_r + (y_0-y_c)y_r + (z_0-z_c)z_r)\\c = (x_0-x_c)^2 + (y_0-y_c)^2 + (z_0-z_c)^2 - r^2\end{split}\]
要找到交點我們只需要找到這個二次方程式的根。它可能是零、一個或兩個實數根。沒有根代表光沒有擊中球體;一個根代表光與球體交於一點 (擦過 「grazing hit」);兩個根代表光與球體交於兩點 (一點交於進入時、一點交於離開時)。在最後一個情況裡,我們想要兩個根之中較小的那個; n 與光離開觀測點的距離成正比,所以先擊中的會是較小的 n 。所以我們呼叫 圖 9.5 的另外兩個函數, 圖 9.6 示範了我們如何產生圖片; (譯註:PGM 可移植灰度圖格式,更多資訊參見 wiki ) (defun ray-test (&optional (res 1))
(setf *world* nil)
(defsphere 0 -300 -1200 200 .8)
(defsphere -80 -150 -1200 200 .7)
(defsphere 70 -100 -1200 200 .9)
(do ((x -2 (1+ x)))
((> x 2))
(do ((z 2 (1+ z)))
((> z 7))
(defsphere (* x 200) 300 (* z -400) 40 .75)))
(tracer (make-pathname :name "spheres.pgm") res))
圖 9.6 使用光線追蹤器 圖 9.7 是產生出來的圖片,其中 ![]() 圖 9.7: 追蹤光線的圖 一個實際的光線追蹤器可以產生更複雜的圖片,因爲它會考慮更多,我們只考慮了單一光源至平面某一點。可能會有多個光源,每一個有不同的強度。它們通常不會在觀測點,在這個情況程式需要檢查至光源的向量是否與其他平面相交,這會在第一個相交的平面上產生陰影。將光源放置於觀測點讓我們不需要考慮這麼複雜的情況,因爲我們看不見在陰影中的任何點。 一個實際的光線追蹤器不僅追蹤光第一個擊中的平面,也會加入其它平面的反射光。一個實際的光線追蹤器會是有顏色的,並可以模型化出透明或是閃耀的平面。但基本的算法會與圖 9.3 所示範的差不多,而許多改進只需要遞迴的使用同樣的成分。 一個實際的光線追蹤器可以是高度優化的。這裡給出的程式爲了精簡寫成,甚至沒有如 Lisp 程式設計師會最佳化的那樣,就僅是一個光線追蹤器而已。僅加入型別與行內宣告 (13.3 節)就可以讓它變得兩倍以上快。 Chapter 9 總結 (Summary)¶
Chapter 9 練習 (Exercises)¶
寫一個程式來模擬這樣的比賽。你的結果實際上有建議委員會每年選出 10 個最佳歌手嗎?
腳註
第十章:宏¶Lisp 程式碼是用 Lisp 物件的列表來表示。2.3 節宣稱這讓 Lisp 可以寫出可自己寫程式的程式。本章將示範如何跨越表達式與程式碼的界線。 10.1 求值 (Eval)¶如何產生表達式是很直觀的:呼叫 > (eval '(+ 1 2 3))
6
> (eval '(format t "Hello"))
Hello
NIL
如果這看起很熟悉的話,這是應該的。這就是我們一直交談的那個 (defun our-toplevel ()
(do ()
(nil)
(format t "~%> ")
(print (eval (read)))))
也是因爲這個原因,頂層也稱爲讀取─求值─打印迴圈 (read-eval-print loop, REPL)。 呼叫
有許多更好的方法 (下一節敘述)來利用產生程式碼的這個可能性。當然 對於程式設計師來說, (defun eval (expr env)
(cond ...
((eql (car expr) 'quote) (cdr expr))
...
(t (apply (symbol-function (car expr))
(mapcar #'(lambda (x)
(eval x env))
(cdr expr))))))
許多表達式由預設子句 (default clause)來處理,預設子句獲得 但是像 函數 > (coerce '(lambda (x) x) 'function)
#<Interpreted-Function BF9D96>
而如果你將 > (compile nil '(lambda (x) (+ x 2)))
#<Compiled-Function BF55BE>
NIL
NIL
由於 函數 10.2 宏 (Macros)¶寫出能寫程式的程式的最普遍方法是通過定義宏。宏是通過轉換 (transformation)而實現的運算子。你通過說明你一個呼叫應該要翻譯成什麼,來定義一個宏。這個翻譯稱爲宏展開(macro-expansion),宏展開由編譯器自動完成。所以宏所產生的程式碼,會變成程式的一個部分,就像你自己輸入的程式一樣。 宏通常透過呼叫 (defmacro nil! (x)
(list 'setf x nil))
這定義了一個新的運算子,稱爲 > (nil! x)
NIL
> x
NIL
完全等同於輸入表達式 要測試一個函數,我們呼叫它,但要測試一個宏,我們看它的展開式 (expansion)。 函數 > (macroexpand-1 '(nil! x))
(SETF X NIL)
T
一個宏呼叫可以展開成另一個宏呼叫。當編譯器(或頂層)遇到一個宏呼叫時,它持續展開它,直到不可展開爲止。 理解宏的祕密是理解它們是如何被實現的。在檯面底下,它們只是轉換成表達式的函數。舉例來說,如果你傳入這個形式 (lambda (expr)
(apply #'(lambda (x) (list 'setf x nil))
(cdr expr)))
它會返回 10.3 反引號 (Backquote)¶反引號讀取宏 (read-macro)使得從模版 (templates)建構列表變得有可能。反引號廣泛使用在宏定義中。一個平常的引用是鍵盤上的右引號 (apostrophe),然而一個反引號是一個左引號。(譯註: open quote 左引號,closed quote 右引號)。它稱作“反引號”是因爲它看起來像是反過來的引號 (titled backwards)。 (譯註: 反引號是鍵盤左上方數字 1 左邊那個: 一個反引號單獨使用時,等於普通的引號: > `(a b c)
(A B C)
和普通引號一樣,單一個反引號保護其參數被求值。 反引號的優點是,在一個反引號表達式裡,你可以使用 > (setf a 1 b 2)
2
> `(a is ,a and b is ,b)
(A IS 1 AND B IS 2)
通過使用反引號取代呼叫 (defmacro nil! (x)
`(setf ,x nil))
> (setf lst '(a b c))
(A B C)
> `(lst is ,lst)
(LST IS (A B C))
> `(its elements are ,@lst)
(ITS ELEMENTS ARE A B C)
> (let ((x 0))
(while (< x 10)
(princ x)
(incf x)))
0123456789
NIL
我們可以通過使用一個剩餘參數,蒐集主體的表達式列表,來定義一個這樣的宏,接著使用 comma-at 來扒開這個列表放至展開式裡: (defmacro while (test &rest body)
`(do ()
((not ,test))
,@body))
10.4 範例:快速排序法(Example: Quicksort)¶圖 10.1 包含了重度依賴宏的一個範例函數 ── 一個使用快速排序演算法 λ 來排序向量的函數。這個函數的工作方式如下: (defun quicksort (vec l r)
(let ((i l)
(j r)
(p (svref vec (round (+ l r) 2)))) ; 1
(while (<= i j) ; 2
(while (< (svref vec i) p) (incf i))
(while (> (svref vec j) p) (decf j))
(when (<= i j)
(rotatef (svref vec i) (svref vec j))
(incf i)
(decf j)))
(if (>= (- j l) 1) (quicksort vec l j)) ; 3
(if (>= (- r i) 1) (quicksort vec i r)))
vec)
圖 10.1 快速排序。
每一次遞迴時,分割越變越小,直到向量完整排序爲止。 在圖 10.1 的實現裡,接受一個向量以及標記欲排序範圍的兩個整數。這個範圍當下的中間元素被選爲主鍵 ( 除了我們前一節定義的 10.5 設計宏 (Macro Design)¶撰寫宏是一種獨特的程式設計,它有著獨一無二的目標與問題。能夠改變編譯器所看到的東西,就像是能夠重寫它一樣。所以當你開始撰寫宏時,你需要像語言設計者一樣思考。 本節快速給出宏所牽涉問題的概要,以及解決它們的技巧。作爲一個例子,我們會定義一個稱爲 > (ntimes 10
(princ "."))
..........
NIL
下面是一個不正確的 (defmacro ntimes (n &rest body)
`(do ((x 0 (+ x 1)))
((>= x ,n))
,@body))
這個定義第一眼看起來可能沒問題。在上面這個情況,它會如預期的工作。但實際上它在兩個方面壞掉了。 一個宏設計者需要考慮的問題之一是,無意的變數捕捉 (variable capture)。這發生在當一個在宏展開式裡用到的變數,恰巧與展開式即將插入的語境裡,有使用同樣名字作爲變數的情況。不正確的 > (let ((x 10))
(ntimes 5
(setf x (+ x 1)))
x)
10
如果 > (let ((x 10))
(do ((x 0 (+ x 1)))
((>= x 5))
(setf x (+ x 1)))
x)
最普遍的解法是不要使用任何可能會被捕捉的一般符號。取而代之的我們使用 gensym (8.4 小節)。因爲 (defmacro ntimes (n &rest body)
(let ((g (gensym)))
`(do ((,g 0 (+ ,g 1)))
((>= ,g ,n))
,@body)))
但這個宏在另一問題上仍有疑慮: 多重求值 (multiple evaluation)。因爲第一個參數被直接插入 > (let ((v 10))
(ntimes (setf v (- v 1))
(princ ".")))
.....
NIL
由於 如果我們看看宏呼叫所展開的表達式,就可以知道爲什麼: > (let ((v 10))
(do ((#:g1 0 (+ #:g1 1)))
((>= #:g1 (setf v (- v 1))))
(princ ".")))
每次迭代我們不是把迭代變數 (gensym 通常印出前面有 避免非預期的多重求值的方法是設置一個變數,在任何迭代前將其設爲有疑惑的那個表達式。這通常牽扯到另一個 gensym: (defmacro ntimes (n &rest body)
(let ((g (gensym))
(h (gensym)))
`(let ((,h ,n))
(do ((,g 0 (+ ,g 1)))
((>= ,g ,h))
,@body))))
終於,這是一個 非預期的變數捕捉與多重求值是折磨宏的主要問題,但不只有這些問題而已。有經驗後,要避免這樣的錯誤與避免更熟悉的錯誤一樣簡單,比如除以零的錯誤。 你的 Common Lisp 實現是一個學習更多有關宏的好地方。藉由呼叫展開至內建宏,你可以理解它們是怎麼寫的。下面是大多數實現對於一個 > (pprint (macroexpand-1 '(cond (a b)
(c d e)
(t f))))
(IF A
B
(IF C
(PROGN D E)
F))
函數 10.6 通用化參照 (Generalized Reference)¶由於一個宏呼叫可以直接在它出現的地方展開成程式碼,任何展開爲 (defmacro cah (lst) `(car ,lst))
然後因爲一個 > (let ((x (list 'a 'b 'c)))
(setf (cah x) 44)
x)
(44 B C)
撰寫一個展開成一個 (defmacro incf (x &optional (y 1)) ; wrong
`(setf ,x (+ ,x ,y)))
但這是行不通的。這兩個表達式不相等: (setf (car (push 1 lst)) (1+ (car (push 1 lst))))
(incf (car (push 1 lst)))
如果 Common Lisp 提供了 (define-modify-macro our-incf (&optional (y 1)) +)
另一版將元素推至列表尾端的 (define-modify-macro append1f (val)
(lambda (lst val) (append lst (list val))))
後者會如下工作: > (let ((lst '(a b c)))
(append1f lst 'd)
lst)
(A B C D)
順道一提, 10.7 範例:實用的宏函數 (Example: Macro Utilities)¶6.4 節介紹了實用函數 (utility)的概念,一種像是構造 Lisp 的通用運算子。我們可以使用宏來定義不能寫作函數的實用函數。我們已經見過幾個例子: (defmacro for (var start stop &body body)
(let ((gstop (gensym)))
`(do ((,var ,start (1+ ,var))
(,gstop ,stop))
((> ,var ,gstop))
,@body)))
(defmacro in (obj &rest choices)
(let ((insym (gensym)))
`(let ((,insym ,obj))
(or ,@(mapcar #'(lambda (c) `(eql ,insym ,c))
choices)))))
(defmacro random-choice (&rest exprs)
`(case (random ,(length exprs))
,@(let ((key -1))
(mapcar #'(lambda (expr)
`(,(incf key) ,expr))
exprs))))
(defmacro avg (&rest args)
`(/ (+ ,@args) ,(length args)))
(defmacro with-gensyms (syms &body body)
`(let ,(mapcar #'(lambda (s)
`(,s (gensym)))
syms)
,@body))
(defmacro aif (test then &optional else)
`(let ((it ,test))
(if it ,then ,else)))
圖 10.2: 實用宏函數 第一個 > (for x 1 8
(princ x))
12345678
NIL
這比寫出等效的 (do ((x 1 (+ x 1)))
((> x 8))
(princ x))
這非常接近實際的展開式: (do ((x 1 (1+ x))
(#:g1 8))
((> x #:g1))
(princ x))
宏需要引入一個額外的變數來持有標記範圍 (range)結束的值。 上面在例子裡的 圖 10.2 的第二個宏 (in (car expr) '+ '- '*)
我們可以改寫成: (let ((op (car expr)))
(or (eql op '+)
(eql op '-)
(eql op '*)))
確實,第一個表達式展開後像是第二個,除了變數 下一個例子 (random-choice (turn-left) (turn-right))
會被展開爲: (case (random 2)
(0 (turn-left))
(1 (turn-right)))
下一個宏 (let ((x (gensym)) (y (gensym)) (z (gensym)))
...)
我們可以寫成 (with-gensyms (x y z)
...)
到目前爲止,圖 10.2 定義的宏,沒有一個可以定義成函數。作爲一個規則,寫成宏是因爲你不能將它寫成函數。但這個規則有幾個例外。有時候你或許想要定義一個運算子來作爲宏,好讓它在編譯期完成它的工作。宏 > (avg 2 4 8)
14/3
是一個這種例子的宏。我們可以將 (defun avg (&rest args)
(/ (apply #'+ args) (length args)))
但它會需要在執行期找出參數的數量。只要我們願意放棄應用 圖 10.2 的最後一個宏是 (let ((val (calculate-something)))
(if val
(1+ val)
0))
我們可以寫成 (aif (calculate-something)
(1+ it)
0)
小心使用 ( Use judiciously),預期的變數捕捉可以是一個無價的技巧。Common Lisp 本身在多處使用它: 舉例來說 像這些宏明確示範了爲何要撰寫替你寫程式的程式。一旦你定義了 如果仍對此懷疑,考慮看看如果你沒有使用任何內建宏時,程式看起來會是怎麼樣。所有宏產生的展開式,你會需要用手產生。你也可以將這個問題用在另一方面。當你在撰寫一個程式時,捫心自問,我需要撰寫宏展開式嗎?如果是的話,宏所產生的展開式就是你需要寫的東西。 10.8 源自 Lisp (On Lisp)¶現在宏已經介紹過了,我們看過更多的 Lisp 是由超乎我們想像的 Lisp 寫成。許多不是函數的 Common Lisp 運算子是宏,而他們全部用 Lisp 寫成的。只有二十五個 Common Lisp 內建的運算子是特殊運算子。 John Foderaro 將 Lisp 稱爲“可程式的程式語言。” λ 通過撰寫你自己的函數與宏,你將 Lisp 變成任何你想要的語言。 (我們會在 17 章看到這個可能性的圖形化示範)無論你的程式適合何種形式,你確信你可以將 Lisp 塑造成適合它的語言。 宏是這個靈活性的主要成分之一。它們允許你將 Lisp 變得完全認不出來,但仍然用一種有原則且高效的方法來實作。在 Lisp 社區裡,宏是個越來越感興趣的主題。可以使用宏辦到驚人之事是很清楚的,但更確信的是宏背後還有更多需要被探索。如果你想的話,可以通過你來發現。Lisp 永遠將進化放在程式設計師手裡。這是它爲什麼存活的原因。 Chapter 10 總結 (Summary)¶
Chapter 10 練習 (Exercises)¶
(a) ((C D) A Z)
(b) (X B C D)
(c) ((C D A) Z)
> (let ((n 2))
(nth-expr n (/ 1 0) (+ 1 2) (/ 1 0)))
3
> (let ((i 0) (n 4))
(n-of n (incf i)))
(1 2 3 4)
(defmacro push (obj lst)
`(setf ,lst (cons ,obj ,lst)))
舉出一個不會與實際 push 做一樣事情的函數呼叫例子。
> (let ((x 1))
(double x)
x)
2
腳註
第十一章:Common Lisp 物件系統¶Common Lisp 物件系統,或稱 CLOS,是一組用來實作物件導向程式設計的運算集。由於它們有著相同的歷史,通常將這些運算子視爲一個群組。 λ 技術上來說,它們與其他部分的 Common Lisp 沒什麼大不同: 11.1 物件導向程式設計 Object-Oriented Programming¶物件導向程式設計代表程式組織方式的改變。這個改變跟已經發生過的處理器運算處理能力分佈的變化雷同。在 1970 年代,一個多用戶的計算機系統,有大量的啞終端(dumb terminal)連接到一個或兩個大型機。現在更可能是用大量相互通過網路連接的工作站來表示。系統的運算處理能力,現在分佈至個體用戶上,而不是集中在一臺大型的計算機上。 物件導向程式設計所帶來的變革與上例非常類似,前者打破了傳統程式的組織方式。不再讓單一的程式去操作那些資料,而是告訴資料自己該做什麼,程式隱含在這些新的資料“物件”的交互過程之中。 舉例來說,假設我們要算出一個二維圖形的面積。一個辦法是寫一個單獨的函數,讓它檢查其參數的型別,然後視型別做處理,如圖 11.1 所示。 (defstruct rectangle
height width)
(defstruct circle
radius)
(defun area (x)
(cond ((rectangle-p x)
(* (rectangle-height x) (rectangle-width x)))
((circle-p x)
(* pi (expt (circle-radius x) 2)))))
> (let ((r (make-rectangle)))
(setf (rectangle-height r) 2
(rectangle-width r) 3)
(area r))
6
圖 11.1: 使用結構及函數來計算面積 使用 CLOS 我們可以寫出一個等效的程式,如圖 11.2 所示。在物件導向模型裡,我們的程式被拆成數個獨一無二的方法,每個方法爲某些特定型別的參數而生。圖 11.2 中的兩個方法,隱性地定義了一個與圖 11.1 相似作用的 (defclass rectangle ()
(height width))
(defclass circle ()
(radius))
(defmethod area ((x rectangle))
(* (slot-value x 'height) (slot-value x 'width)))
(defmethod area ((x circle))
(* pi (expt (slot-value x 'radius) 2)))
> (let ((r (make-instance 'rectangle)))
(setf (slot-value r 'height) 2
(slot-value r 'width) 3)
(area r))
6
圖 11.2: 使用型別與方法來計算面積 通過這種方式,我們將函數拆成獨一無二的方法,面向物件暗指繼承 (inheritance) ── 槽(slot)與方法(method)皆有繼承。在圖 11.2 中,作爲第二個參數傳給 (defclass colored ()
(color))
(defclass colored-circle (circle colored)
())
當我們創造
從實踐層面來看,物件導向程式設計代表著以方法、類、實體以及繼承來組織程式。爲什麼你會想這麼組織程式?面向物件方法的主張之一說這樣使得程式更容易改動。如果我們想要改變 11.2 類與實體 (Class and Instances)¶在 4.6 節時,我們看過了創建結構的兩個步驟:我們呼叫 (defclass circle ()
(radius center))
這個定義說明了 要創建這個類的實體,我們呼叫通用的 > (setf c (make-instance 'circle))
#<CIRCLE #XC27496>
要給這個實體的槽賦值,我們可以使用 > (setf (slot-value c 'radius) 1)
1
與結構的欄位類似,未初始化的槽的值是未定義的 (undefined)。 11.3 槽的屬性 (Slot Properties)¶傳給 通過替一個槽定義一個存取器 (accessor),我們隱式地定義了一個可以引用到槽的函數,使我們不需要再呼叫 (defclass circle ()
((radius :accessor circle-radius)
(center :accessor circle-center)))
那我們能夠分別通過 > (setf c (make-instance 'circle))
#<CIRCLE #XC5C726>
> (setf (circle-radius c) 1)
1
> (circle-radius c)
1
通過指定一個 要指定一個槽的預設值,我們可以給入一個 (defclass circle ()
((radius :accessor circle-radius
:initarg :radius
:initform 1)
(center :accessor circle-center
:initarg :center
:initform (cons 0 0))))
現在當我們創建一個 > (setf c (make-instance 'circle :radius 3))
#<CIRCLE #XC2DE0E>
> (circle-radius c)
3
> (circle-center c)
(0 . 0)
注意 我們可以指定某些槽是共享的 ── 也就是每個產生出來的實體,共享槽的值都會是一樣的。我們通過宣告槽擁有 舉例來說,假設我們想要模擬一羣成人小報 (a flock of tabloids)的行爲。(譯註:可以看看什麼是 tabloids。)在我們的模擬中,我們想要能夠表示一個事實,也就是當一家小報採用一個頭條時,其它小報也會跟進的這個行爲。我們可以通過讓所有的實體共享一個槽來實現。若 (defclass tabloid ()
((top-story :accessor tabloid-story
:allocation :class)))
那麼如果我們創立兩家小報,無論一家的頭條是什麼,另一家的頭條也會是一樣的: > (setf daily-blab (make-instance 'tabloid)
unsolicited-mail (make-instance 'tabloid))
#<TABLOID #x302000EFE5BD>
> (setf (tabloid-story daily-blab) 'adultery-of-senator)
ADULTERY-OF-SENATOR
> (tabloid-story unsolicited-mail)
ADULTERY-OF-SENATOR
譯註: ADULTERY-OF-SENATOR 參議員的性醜聞。 若有給入 11.4 基類 (Superclasses)¶
(defclass graphic ()
((color :accessor graphic-color :initarg :color)
(visible :accessor graphic-visible :initarg :visible
:initform t)))
(defclass screen-circle (circle graphic) ())
則 存取器及 > (graphic-color (make-instance 'screen-circle
:color 'red :radius 3))
RED
我們可以使每一個 (defclass screen-circle (circle graphic)
((color :initform 'purple)))
現在 > (graphic-color (make-instance 'screen-circle))
PURPLE
11.5 優先序 (Precedence)¶我們已經看過類別是怎樣能有多個基類了。當一個實體的方法同時屬於這個實體所屬的幾個類時,Lisp 需要某種方式來決定要使用哪個方法。優先序的重點在於確保這一切是以一種直觀的方式發生的。 每一個類別,都有一個優先序列表:一個將自身及自身的基類從最具體到最不具體所排序的列表。在目前看過的例子中,優先序還不是需要討論的議題,但在更大的程式裡,它會是一個需要考慮的議題。 以下是一個更複雜的類別層級: (defclass sculpture () (height width depth))
(defclass statue (sclpture) (subject))
(defclass metalwork () (metal-type))
(defclass casting (metalwork) ())
(defclass cast-statue (statue casting) ())
圖 11.3 包含了一個表示 ![]() 圖 11.3: 類別層級 要替一個類別建構一個這樣的網路,從最底層用一個節點表示該類別開始。接著替類別最近的基類畫上節點,其順序根據 一個類別的優先序列表可以通過如下步驟,遍歷對應的網路計算出來:
這個定義的結果之一(實際上講的是規則 3)在優先序列表裡,類別不會在其子類別出現前出現。 圖 11.3 的箭頭示範了一個網路是如何遍歷的。由這個圖所決定出的優先序列表爲: 優先序的主要目的是,當一個通用函數 (generic function)被呼叫時,決定要用哪個方法。這個過程在下一節講述。另一個優先序重要的地方是,當一個槽從多個基類繼承時。408 頁的備註解釋了當這情況發生時的應用規則。 λ 11.6 通用函數 (Generic Functions)¶一個通用函數 (generic function) 是由一個或多個方法組成的一個函數。方法可用 (defmethod combine (x y)
(list x y))
現在 > (combine 'a 'b)
(A B)
到現在我們還沒有做任何一般函數做不到的事情。一個通用函數不尋常的地方是,我們可以繼續替它加入新的方法。 首先,我們定義一些可以讓新的方法引用的類別: (defclass stuff () ((name :accessor name :initarg :name)))
(defclass ice-cream (stuff) ())
(defclass topping (stuff) ())
這裡定義了三個類別: 現在下面是替 (defmethod combine ((ic ice-cream) (top topping))
(format nil "~A ice-cream with ~A topping."
(name ic)
(name top)))
在這次 而當一個通用函數被呼叫時, Lisp 是怎麼決定要用哪個方法的?Lisp 會使用參數的類別與參數的特化匹配且優先序最高的方法。這表示若我們用 > (combine (make-instance 'ice-cream :name 'fig)
(make-instance 'topping :name 'treacle))
"FIG ice-cream with TREACLE topping"
但使用其他參數時,我們會得到我們第一次定義的方法: > (combine 23 'skiddoo)
(23 SKIDDOO)
因爲第一個方法的兩個參數皆沒有特化,它永遠只有最低優先權,並永遠是最後一個呼叫的方法。一個未特化的方法是一個安全手段,就像 一個方法中,任何參數的組合都可以特化。在這個方法裡,只有第一個參數被特化了: (defmethod combine ((ic ice-cream) x)
(format nil "~A ice-cream with ~A."
(name ic)
x))
若我們用一個 > (combine (make-instance 'ice-cream :name 'grape)
(make-instance 'topping :name 'marshmallow))
"GRAPE ice-cream with MARSHMALLOW topping"
然而若第一個參數是 > (combine (make-instance 'ice-cream :name 'clam)
'reluctance)
"CLAM ice-cream with RELUCTANCE"
當一個通用函數被呼叫時,參數決定了一個或多個可用的方法 (applicable methods)。如果在呼叫中的參數在參數的特化約定內,我們說一個方法是可用的。 如果沒有可用的方法,我們會得到一個錯誤。如果只有一個,它會被呼叫。如果多於一個,最具體的會被呼叫。最具體可用的方法是由呼叫傳入參數所屬類別的優先序所決定的。由左往右審視參數。如果有一個可用方法的第一個參數,此參數特化給某個類,其類的優先序高於其它可用方法的第一個參數,則此方法就是最具體的可用方法。平手時比較第二個參數,以此類推。 [2] 在前面的例子裡,很容易看出哪個是最具體的可用方法,因爲所有的物件都是單繼承的。一個 方法不需要在由 (defmethod combine ((x number) (y number))
(+ x y))
方法甚至可以對單一的物件做特化,用 (defmethod combine ((x (eql 'powder)) (y (eql 'spark)))
'boom)
單一物件特化的優先序比類別特化來得高。 方法可以像一般 Common Lisp 函數一樣有複雜的參數列表,但所有組成通用函數方法的參數列表必須是一致的 (congruent)。參數的數量必須一致,同樣數量的選擇性參數(如果有的話),要嘛一起使用 (x) (a)
(x &optional y) (a &optional b)
(x y &rest z) (a b &key c)
(x y &key z) (a b &key c d)
而下列的參數列表對不是一致的: (x) (a b)
(x &optional y) (a &optional b c)
(x &optional y) (a &rest b)
(x &key x y) (a)
只有必要參數可以被特化。所以每個方法都可以通過名字及必要參數的特化獨一無二地識別出來。如果我們定義另一個方法,有著同樣的修飾符及特化,它會覆寫掉原先的。所以通過說明 (defmethod combine ((x (eql 'powder)) (y (eql 'spark)))
'kaboom)
我們重定義了當 11.7 輔助方法 (Auxiliary Methods)¶方法可以透過如
這稱爲標準方法組合機制 (standard method combination)。在標準方法組合機制裡,呼叫一個通用函數會呼叫
返回值爲 輔助方法通過在 (defclass speaker () ())
(defmethod speak ((s speaker) string)
(format t "~A" string))
則使用 > (speak (make-instance 'speaker)
"I'm hungry")
I'm hungry
NIL
通過定義一個 (defclass intellectual (speaker) ())
(defmethod speak :before ((i intellectual) string)
(princ "Perhaps "))
(defmethod speak :after ((i intellectual) string)
(princ " in some sense"))
我們可以創建一個說話前後帶有慣用語的演講者: > (speak (make-instance 'intellectual)
"I am hungry")
Perhaps I am hungry in some sense
NIL
如同先前標準方法組合機制所述,所有的 (defmethod speak :before ((s speaker) string)
(princ "I think "))
無論是哪個 而在有 有了 (defclass courtier (speaker) ())
(defmethod speak :around ((c courtier) string)
(format t "Does the King believe that ~A?" string)
(if (eql (read) 'yes)
(if (next-method-p) (call-next-method))
(format t "Indeed, it is a preposterous idea. ~%"))
'bow)
當傳給 > (speak (make-instance 'courtier) "kings will last")
Does the King believe that kings will last? yes
I think kings will last
BOW
> (speak (make-instance 'courtier) "kings will last")
Does the King believe that kings will last? no
Indeed, it is a preposterous idea.
BOW
記得由 11.8 方法組合機制 (Method Combination)¶在標準方法組合中,只有最具體的主方法會被呼叫(雖然它可以通過 用其它組合手段來定義方法也是有可能的 ── 舉例來說,一個返回所有可用主方法的和的通用函數。運算子 (Operator)方法組合可以這麼理解,想像它是 Lisp 表達式的求值後的結果,其中 Lisp 表達式的第一個元素是某個運算子,而參數是按照具體性呼叫可用主方法的結果。如果我們定義 (defun price (&rest args)
(+ (apply 〈most specific primary method〉 args)
.
.
.
(apply 〈least specific primary method〉 args)))
如果有可用的 我們可以指定一個通用函數的方法組合所要使用的型別,藉由在 (defgeneric price (x)
(:method-combination +))
現在 (defclass jacket () ())
(defclass trousers () ())
(defclass suit (jacket trousers) ())
(defmethod price + ((jk jacket)) 350)
(defmethod price + ((tr trousers)) 200)
則可以獲得一件西裝的價格,也就是所有可用方法的總和: > (price (make-instance 'suit))
550
下列符號可以用來作爲 + and append list max min nconc or progn
你也可以使用 一旦你指定了通用函數要用何種方法組合,所有替該函數定義的方法必須用同樣的機制。而現在如果我們試著使用另個運算子( 而現在如果我們試著使用另個運算子( 11.9 封裝 (Encapsulation)¶面向物件的語言通常會提供某些手段,來區別物件的表示法以及它們給外在世界存取的介面。隱藏實現細節帶來兩個優點:你可以改變實現方式,而不影響物件對外的樣子,而你可以保護物件在可能的危險方面被改動。隱藏細節有時候被稱爲封裝 (encapsulated)。 雖然封裝通常與物件導向程式設計相關聯,但這兩個概念其實是沒相乾的。你可以只擁有其一,而不需要另一個。我們已經在 108 頁 (譯註: 6.5 小節。)看過一個小規模的封裝例子。函數 在 Common Lisp 裡,包是標準的手段來區分公開及私有的資訊。要限制某個東西的存取,我們將它放在另一個包裡,並且針對外部介面,僅輸出需要用的名字。 我們可以通過輸出可被改動的名字,來封裝一個槽,但不是槽的名字。舉例來說,我們可以定義一個 (defpackage "CTR"
(:use "COMMON-LISP")
(:export "COUNTER" "INCREMENT" "CLEAR"))
(in-package ctr)
(defclass counter () ((state :initform 0)))
(defmethod increment ((c counter))
(incf (slot-value c 'state)))
(defmethod clear ((c counter))
(setf (slot-value c 'state) 0))
在這個定義下,在包外部的程式只能夠創造 如果你想要更進一步區別類的內部及外部介面,並使其不可能存取一個槽所存的值,你也可以這麼做。只要在你將所有需要引用它的程式碼定義完,將槽的名字 (unintern 'state)
則沒有任何合法的、其它的辦法,從任何包來引用到這個槽。 λ 11.10 兩種模型 (Two Models)¶物件導向程式設計是一個令人疑惑的話題,部分的原因是因爲有兩種實現方式:訊息傳遞模型 (message-passing model)與通用函數模型 (generic function model)。一開始先有的訊息傳遞。通用函數是廣義的訊息傳遞。 在訊息傳遞模型裡,方法屬於物件,且方法的繼承與槽的繼承概念一樣。要找到一個物體的面積,我們傳給它一個 tell obj area
而這呼叫了任何物件 有時候我們需要傳入額外的參數。舉例來說,一個 (move obj 10)
訊息傳遞模型的侷限性變得清晰。在訊息傳遞模型裡,我們僅特化 (specialize) 第一個參數。 牽扯到多物件時,沒有規則告訴方法該如何處理 ── 而物件回應消息的這個模型使得這更加難處理了。 在訊息傳遞模型裡,方法是物件所有的,而在通用函數模型裡,方法是特別爲物件打造的 (specialized)。 如果我們僅特化第一個參數,那麼通用函數模型和訊息傳遞模型就是一樣的。但在通用函數模型裡,我們可以更進一步,要特化幾個參數就幾個。這也表示了,功能上來說,訊息傳遞模型是通用函數模型的子集。如果你有通用函數模型,你可以僅特化第一個參數來模擬出訊息傳遞模型。 Chapter 11 總結 (Summary)¶
Chapter 11 練習 (Exercises)¶
(defclass a (c d) ...) (defclass e () ...)
(defclass b (d c) ...) (defclass f (h) ...)
(defclass c () ...) (defclass g (h) ...)
(defclass d (e f g) ...) (defclass h () ...)
使用這些函數(不要使用
腳註
第十二章:結構¶3.3 節中介紹了 Lisp 如何使用指標允許我們將任何值放到任何地方。這種說法是完全有可能的,但這並不一定都是好事。 例如,一個物件可以是它自已的一個元素。這是好事還是壞事,取決於程式設計師是不是有意這樣設計的。 12.2 修改 (Modification)¶爲什麼要避免共享結構呢?之前討論的共享結構問題僅僅是個智力練習,到目前爲止,並沒使我們在實際寫程式的時候有什麼不同。當修改一個被共享的結構時,問題出現了。如果兩個列表共享結構,當我們修改了其中一個,另外一個也會無意中被修改。 上一節中,我們介紹了怎樣構建一個是其它列表的尾端的列表: (setf whole (list 'a 'b 'c)
tail (cdr whole))
因爲 > (setf (second tail ) 'e)
E
> tail
(B E)
> whole
(A B E)
同樣的,如果兩個列表共享同一個尾端,這種情況也會發生。 一次修改兩個物件並不總是錯誤的。有時候這可能正是你想要的。但如果無意的修改了共享結構,將會引入一些非常微妙的 bug。Lisp 程式設計師要培養對共享結構的意識,並且在這類錯誤發生時能夠立刻反應過來。當一個列表神祕的改變了的時候,很有可能是因爲改變了其它與之共享結構的物件。 真正危險的不是共享結構,而是改變被共享的結構。爲了安全起見,乾脆避免對結構使用 當你呼叫別人寫的函數的時候要加倍小心。除非你知道它內部的操作,否則,你傳入的參數時要考慮到以下的情況: 1.它對你傳入的參數可能會有破壞性的操作 2.你傳入的參數可能被保存起來,如果你呼叫了一個函數,然後又修改了之前作爲參數傳入該函數的物件,那麼你也就改變了函數已保存起來作爲它用的物件[1]。 在這兩種情況下,解決的方法是傳入一個拷貝。 在 Common Lisp 中,一個函數呼叫在遍歷列表結構 (比如, 12.3 範例:佇列 (Example: Queues)¶共享結構並不是一個總讓人擔心的特性。我們也可以對其加以利用的。這一節展示了怎樣用共享結構來表示佇列。佇列物件是我們可以按照資料的插入順序逐個檢出資料的倉庫,這個規則叫做先進先出 (FIFO, first in, first out)。 用列表表示棧 (stack)比較容易,因爲棧是從同一端插入和檢出。而表示佇列要困難些,因爲佇列的插入和檢出是在不同端。爲了有效的實現佇列,我們需要找到一種辦法來指向列表的兩個端。 圖 12.6 給出了一種可行的策略。它展示怎樣表示一個含有 a,b,c 三個元素的佇列。一個佇列就是一對列表,最後那個 ![]() 圖 12.6 一個佇列的結構 (defun make-queue () (cons nil nil))
(defun enqueue (obj q)
(if (null (car q))
(setf (cdr q) (setf (car q) (list obj)))
(setf (cdr (cdr q)) (list obj)
(cdr q) (cdr (cdr q))))
(car q))
(defun dequeue (q)
(pop (car q)))
圖 12.7 佇列實現 圖 12.7 中的程式實現了這一策略。其用法如下: > (setf q1 (make-queue))
(NIL)
> (progn (enqueue 'a q1)
(enqueue 'b q1)
(enqueue 'c q1))
(A B C)
現在, > q1
((A B C) C)
從佇列中檢出一些元素: > (dequeue q1)
A
> (dequeue q1)
B
> (enqueue 'd q1)
(C D)
12.4 破壞性函數 (Destructive Functions)¶Common Lisp 包含一些允許修改列表結構的函數。爲了提高效率,這些函數是具有破壞性的。雖然它們可以回收利用作爲參數傳給它們的 比如, > (setf lst '(a r a b i a) )
(A R A B I A)
> (delete 'a lst )
(R B I)
> lst
(A R B I)
正如 (setf lst (delete 'a lst))
破壞性函數是怎樣回收利用傳給它們的列表的呢?比如,可以考慮 (defun nconc2 ( x y)
(if (consp x)
(progn
(setf (cdr (last x)) y)
x)
y))
我們找到第一個列表的最後一個 Cons 核 (cons cells),把它的 函數 > (mapcan #'list
'(a b c)
'(1 2 3 4))
( A 1 B 2 C 3)
這個函數可以定義如下: (defun our-mapcan (fn &rest lsts )
(apply #'nconc (apply #'mapcar fn lsts)))
使用 這類函數在處理某些問題的時候特別有用,比如,收集樹在某層上的所有子結點。如果 (defun grandchildren (x)
(mapcan #'(lambda (c)
(copy-list (children c)))
(children x)))
這個函數呼叫 一個 (defun mappend (fn &rest lsts )
(apply #'append (apply #'mapcar fn lsts)))
如果使用 (defun grandchildren (x)
(mappend #'children (children x)))
12.5 範例:二元搜索樹 (Example: Binary Search Trees)¶在某些情況下,使用破壞性操作比使用非破壞性的顯得更自然。第 4.7 節中展示了如何維護一個具有二分搜索格式的有序物件集 (或者說維護一個二元搜索樹 (BST))。第 4.7 節中給出的函數都是非破壞性的,但在我們真正使用BST的時候,這是一個不必要的保護措施。本節將展示如何定義更符合實際應用的具有破壞性的插入函數和刪除函數。 圖 12.8 示範了如何定義一個具有破壞性的 > (setf *bst* nil)
NIL
> (dolist (x '(7 2 9 8 4 1 5 12))
(setf *bst* (bst-insert! x *bst* #'<)))
NIL
(defun bst-insert! (obj bst <)
(if (null bst)
(make-node :elt obj)
(progn (bsti obj bst <)
bst)))
(defun bsti (obj bst <)
(let ((elt (node-elt bst)))
(if (eql obj elt)
bst
(if (funcall < obj elt)
(let ((l (node-l bst)))
(if l
(bsti obj l <)
(setf (node-l bst)
(make-node :elt obj))))
(let ((r (node-r bst)))
(if r
(bsti obj r <)
(setf (node-r bst)
(make-node :elt obj))))))))
圖 12.8: 二元搜索樹:破壞性插入 你也可以爲 BST 定義一個類似 push 的功能,但這超出了本書的範圍。(好奇的話,可以參考第 409 頁 「譯者注:即備註 204 」 的宏定義。) 與 > (setf *bst* (bst-delete 2 *bst* #'<) )
#<7>
> (bst-find 2 *bst* #'<)
NIL
(defun bst-delete (obj bst <)
(if bst (bstd obj bst nil nil <))
bst)
(defun bstd (obj bst prev dir <)
(let ((elt (node-elt bst)))
(if (eql elt obj)
(let ((rest (percolate! bst)))
(case dir
(:l (setf (node-l prev) rest))
(:r (setf (node-r prev) rest))))
(if (funcall < obj elt)
(if (node-l bst)
(bstd obj (node-l bst) bst :l <))
(if (node-r bst)
(bstd obj (node-r bst) bst :r <))))))
(defun percolate! (bst)
(cond ((null (node-l bst))
(if (null (node-r bst))
nil
(rperc! bst)))
((null (node-r bst)) (lperc! bst))
(t (if (zerop (random 2))
(lperc! bst)
(rperc! bst)))))
(defun lperc! (bst)
(setf (node-elt bst) (node-elt (node-l bst)))
(percolate! (node-l bst)))
(defun rperc! (bst)
(setf (node-elt bst) (node-elt (node-r bst)))
(percolate! (node-r bst)))
圖 12.9: 二元搜索樹:破壞性刪除 譯註: 此範例已被回報爲錯誤的,一個修復的版本請造訪這裡。 12.6 範例:雙向鏈表 (Example: Doubly-Linked Lists)¶普通的 Lisp 列表是單向鏈表,這意味著其指標指向一個方向:我們可以獲取下一個元素,但不能獲取前一個。在雙向鏈表中,指標指向兩個方向,我們獲取前一個元素和下一個元素都很容易。這一節將介紹如何創建和操作雙向鏈表。 圖 12.10 示範了如何用結構來實現雙向鏈表。將 (defstruct (dl (:print-function print-dl))
prev data next)
(defun print-dl (dl stream depth)
(declare (ignore depth))
(format stream "#<DL ~A>" (dl->list dl)))
(defun dl->list (lst)
(if (dl-p lst)
(cons (dl-data lst) (dl->list (dl-next lst)))
lst))
(defun dl-insert (x lst)
(let ((elt (make-dl :data x :next lst)))
(when (dl-p lst)
(if (dl-prev lst)
(setf (dl-next (dl-prev lst)) elt
(dl-prev elt) (dl-prev lst)))
(setf (dl-prev lst) elt))
elt))
(defun dl-list (&rest args)
(reduce #'dl-insert args
:from-end t :initial-value nil))
(defun dl-remove (lst)
(if (dl-prev lst)
(setf (dl-next (dl-prev lst)) (dl-next lst)))
(if (dl-next lst)
(setf (dl-prev (dl-next lst)) (dl-prev lst)))
(dl-next lst))
圖 12.10: 構造雙向鏈表 ![]() 圖 12.11: 一個雙向鏈表。 爲了便於操作,我們爲雙向鏈表定義了一些實現類似 函數 幾個普通列表可以共享同一個尾端。因爲雙向鏈表的尾端不得不指向它的前一個元素,所以不可能存在兩個雙向鏈表共享同一個尾端。如果 單向鏈表(普通列表)和雙向鏈表另一個有趣的區別是,如何持有它們。我們使用普通列表的首端,來表示單向鏈表,如果將列表賦值給一個變數,變數可以通過保存指向列表第一個 函數 > (dl-list 'a 'b 'c)
#<DL (A B C)>
它使用了 (dl-insert 'a (dl-insert 'b (dl-insert 'c nil)) )
如果將 > (setf dl (dl-list 'a 'b))
#<DL (A B)>
> (setf dl (dl-insert 'c dl))
#<DL (C A B)>
> (dl-insert 'r (dl-next dl))
#<DL (R A B)>
> dl
#<DL (C R A B)>
最後, 12.7 環狀結構 (Circular Structure)¶將列表結構稍微修改一下,就可以得到一個環形列表。存在兩種環形列表。最常用的一種是其頂層列表結構是一個環的,我們把它叫做 構造一個單元素的 > (setf x (list 'a))
(A)
> (progn (setf (cdr x) x) nil)
NIL
這樣 ![]() 圖 12.12 環狀列表。 如果 Lisp 試著打印我們剛剛構造的結構,將會顯示 (a a a a a …… —— 無限個 > (setf *print-circle* t )
T
> x
#1=(A . #1#)
如果你需要,你也可以使用
(defun circular (lst)
(setf (cdr (last lst)) lst))
另外一種環狀列表叫做 > (let ((y (list 'a )))
(setf (car y) y)
y)
#i=(#i#)
圖 12.12 (右) 展示了其結構。這個 一個列表也可以既是 > (let ((c (cons 11)) )
(setf (car c) c
(cdr c) c)
c)
#1=(#1# . #1#)
很難想像這樣的一個列表有什麼用。實際上,了解環形列表的主要目的就是爲了避免因爲偶然因素構造出了環形列表,因爲,將一個環形列表傳給一個函數,如果該函數遍歷這個環形列表,它將進入死迴圈。 環形結構的這種問題在列表以外的其他物件中也存在。比如,一個陣列可以將陣列自身當作其元素: > (setf *print-array* t )
T
> (let ((a (make-array 1)) )
(setf (aref a 0) a)
a)
#1=#(#1#)
實際上,任何可以包含元素的物件都可能包含其自身作爲元素。 用 > (progn (defstruct elt
(parent nil ) (child nil) )
(let ((c (make-elt) )
(p (make-elt)) )
(setf (elt-parent c) p
(elt-child p) c)
c) )
#1=#S(ELT PARENT #S(ELT PARENT NIL CHILD #1#) CHILD NIL)
要實現像這樣一個結構的打印函數 ( 12.8 常數結構 (Constant Structure)¶因爲常數實際上是程式碼的一部分,所以我們也不應該修改常數,或者你可能不經意地寫了自重寫的程式碼。一個通過 (defun arith-op (x)
(member x '(+ - * /)))
如果被測試的符號是算術運算符,它的返回值將至少一個被引用列表的一部分。如果我們修改了其返回值, > (nconc (arith-op '*) '(as i t were))
(* / AS IT WERE)
那麼我就會修改 > (arith-op 'as )
(AS IT WERE)
寫一個返回常數結構的函數,並不一定是錯誤的。但當你考慮使用一個破壞性的操作是否安全的時候,你必須考慮到這一點。 有幾個其它方法來實現 (defun arith-op (x)
(member x (list '+ '- '* '/)))
這裡,使用 (defun arith-op (x)
(find x '(+ - * /)))
這一節討論的問題似乎只與列表有關,但實際上,這個問題存在於任何複雜的物件中:陣列,字元串,結構,實體等。你不應該逐字地去修改程式的程式碼段。 即使你想寫自修改程式,通過修改常數來實現並不是個好辦法。編譯器將常數編譯成了程式碼,破壞性的操作可能修改它們的參數,但這些都是沒有任何保證的事情。如果你想寫自修改程式,正確的方法是使用閉包 (見 6.5 節)。 Chapter 12 總結 (Summary)¶
Chapter 12 練習 (Exercises)¶
> (setf q (make-queue))
(NIL)
> (enqueue 'a q)
(A)
> (enqueue 'b q)
(A B)
> (dequeue q)
A
腳註
第十三章:速度¶Lisp 實際上是兩種語言:一種能寫出快速執行的程式,一種則能讓你快速的寫出程式。 在程式開發的早期階段,你可以爲了開發上的便捷捨棄程式的執行速度。一旦程式的結構開始固化,你就可以精煉其中的關鍵部分以使得它們執行的更快。 由於各個 Common Lisp 實現間的差異,很難針對優化給出通用的建議。在一個實現上使程式變快的修改也許在另一個實現上會使得程式變慢。這是難免的事兒。越強大的語言,離機器底層就越遠,離機器底層越遠,語言的不同實現沿著不同路徑趨向它的可能性就越大。因此,即便有一些技巧幾乎一定能夠讓程式運行的更快,本章的目的也只是建議而不是規定。 13.1 瓶頸規則 (The Bottleneck Rule)¶不管是什麼實現,關於優化都可以整理出三點規則:它應該關注瓶頸,它不應該開始的太早,它應該始於算法。 也許關於優化最重要的事情就是要意識到,程式中的大部分執行時間都是被少數瓶頸所消耗掉的。 正如高德納所說,“在一個與 I/O 無關 (Non-I/O bound) 的程式中,大部分的運行時間集中在大概 3% 的原始碼中。” λ 優化程式的這一部分將會使得它的運行速度明顯的提升;相反,優化程式的其他部分則是在浪費時間。 因此,優化程式時關鍵的第一步就是找到瓶頸。許多 Lisp 實現都提供性能分析器 (profiler) 來監視程式的運行並報告每一部分所花費的時間量。 爲了寫出最爲高效的程式,性能分析器非常重要,甚至是必不可少的。 如果你所使用的 Lisp 實現帶有性能分析器,那麼請在進行優化時使用它。另一方面,如果實現沒有提供性能分析器的話,那麼你就不得不通過猜測來尋找瓶頸,而且這種猜測往往都是錯的! 瓶頸規則的一個推論是,不應該在程式的初期花費太多的精力在優化上。高德納對此深信不疑:“過早的優化是一切 (至少是大多數) 問題的源頭。” λ 在剛開始寫程式的時候,通常很難看清真正的瓶頸在哪,如果這個時候進行優化,你很可能是在浪費時間。優化也會使程式的修改變得更加困難,邊寫程式邊優化就像是在用風乾非常快的顏料來畫畫一樣。 在適當的時候做適當的事情,可以讓你寫出更優秀的程式。Lisp 的一個優點就是能讓你用兩種不同的工作方式來進行開發:快速地寫出執行較慢的程式,或者,放慢寫程式的速度,精雕細琢,從而得出執行得較快的程式。 在程式開發的初期階段,工作通常在第一種模式下進行,只有當性能成爲問題的時候,才切換到第二種模式。 對於非常底層的語言,比如彙編,你必須優化程式的每一行。但這麼做會浪費你大部分的精力,因爲瓶頸僅僅是其中很小的那部分程式。一個更加抽象的語言能夠讓你把主要精力集中在瓶頸上, 達到事半功倍的效果。 當真正開始優化的時候,還必須從最頂端入手。 在使用各種低層次的編碼技巧 (low-level coding tricks) 之前,請先確保你已經使用了最爲高效的算法。 這麼做的潛在好處相當大 ── 甚至可能大到你都不再需要玩那些奇淫技巧。 當然本規則還是要和前一個規則保持平衡。 有些時候,關於算法的決策必須儘早進行。 13.2 編譯 (Compilation)¶有五個參數可以控制程式的編譯方式: speed (速度)代表編譯器產生程式碼的速度; compilation-speed (編譯速度)代表程式被編譯的速度; safety (安全) 代表要對目標程式碼進行錯誤檢查的數量; space (空間)代表目標程式碼的大小和記憶體需求量;最後, debug (除錯)代表爲了除錯而保留的資訊量。 Note 互動與直譯 (INTERACTIVE VS. INTERPRETED) Lisp 是一種互動式語言 (Interactive Language),但是互動式的語言不必都是直譯型的。早期的 Lisp 都通過直譯器實現,因此認爲 Lisp 的特質都依賴於它是被直譯的想法就這麼產生了。但這種想法是錯誤的:Common Lisp 既是編譯型語言,又是直譯型語言。 至少有兩種 Common Lisp 實現甚至都不包含直譯器。在這些實現中,輸入到頂層的表達式在求值前會被編譯。因此,把頂層叫做直譯器的這種說法,不僅是落伍的,甚至還是錯誤的。 編譯參數不是真正的變數。它們在宣告中被分配從 (defun bottleneck (...)
(do (...)
(...)
(do (...)
(...)
(declare (optimize (speed 3) (safety 0)))
...)))
一般情況下,應該在程式寫完並且經過完善測試之後,才考慮加上那麼一句宣告。 要讓程式在任何情況下都儘可能地快,可以使用如下宣告: (declaim (optimize (speed 3)
(compilation-speed 0)
(safety 0)
(debug 0)))
考慮到前面提到的瓶頸規則 [1] ,這種苛刻的做法可能並沒有什麼必要。 另一種特別重要的優化就是由 Lisp 編譯器完成的尾遞迴優化。當 speed (速度)的權值最大時,所有支持尾遞迴優化的編譯器都將保證對程式碼進行這種優化。 如果在一個呼叫返回時呼叫者中沒有殘餘的計算,該呼叫就被稱爲尾遞迴。下面的程式返回列表的長度: (defun length/r (lst)
(if (null lst)
0
(1+ (length/r (cdr lst)))))
這個遞迴呼叫不是尾遞迴,因爲當它返回以後,它的值必須傳給 (defun length/rt (lst)
(labels ((len (lst acc)
(if (null lst)
acc
(len (cdr lst) (1+ acc)))))
(len lst 0)))
更準確地說,區域函數 出色的編譯器能夠將一個尾遞迴編譯成一個跳轉 (goto),因此也能將一個尾遞迴函數編譯成一個迴圈。在典型的機器語言程式碼中,當第一次執行到表示 另一個利用函數呼叫抽象,卻又沒有開銷的方法是使函數內聯編譯。對於那些呼叫開銷比函數體的執行代價還高的小型函數來說,這種技術非常有價值。例如,以下程式用來判斷列表是否僅有一個元素: (declaim (inline single?))
(defun single? (lst)
(and (consp lst) (null (cdr lst))))
因爲這個函數是在全局被宣告爲內聯的,引用了 (defun foo (x)
(single? (bar x)))
當 (defun foo (x)
(let ((lst (bar x)))
(and (consp lst) (null (cdr lst)))))
內聯編譯有兩個限制。首先,遞迴函數不能內聯。其次,如果一個內聯函數被重新定義,我們就必須重新編譯呼叫它的任何函數,否則呼叫仍然使用原來的定義。 在一些早期的 Lisp 方言中,有時候會使用宏( 10.2 節)來避免函數呼叫。這種做法在 Common Lisp 中通常是沒有必要的。 不同 Lisp 編譯器的優化方式千差萬別。如果你想了解你的編譯器爲某個函數生成的程式碼,試著呼叫 13.3 型別宣告 (Type Declarations)¶如果 Lisp 不是你所學的第一門編程語言,那麼你也許會感到困惑,爲什麼這本書還沒說到型別宣告這件事來?畢竟,在很多流行的編程語言中,型別宣告是必須要做的。 在多數編程語言裡,你必須爲每個變數宣告型別,並且變數也只可以持有與該型別相一致的值。這種語言被稱爲強型別(strongly typed) 語言。除了給程式設計師們徒增了許多負擔外,這種方式還限制了你能做的事情。使用這種語言,很難寫出那些需要多種型別的參數一起工作的函數,也很難定義出可以包含不同種類元素的資料結構。當然,這種方式也有它的優勢,比如無論何時當編譯器碰到一個加法運算,它都能夠事先知道這是一個什麼型別的加法運算。如果兩個參數都是整數型別,編譯器可以直接在目標程式碼中生成一個固定 (hard-wire) 的整數加法運算。 正如 2.15 節所講,Common Lisp 使用一種更加靈活的方式:顯式型別 (manifest typing) [3] 。有型別的是值而不是變數。變數可以用於任何型別的物件。 當然,這種靈活性需要付出一定的速度作爲代價。由於 在某些時候,如果我們要執行的全都是整數的加法,那麼每次查看參數型別的這種做法就說不上高效了。Common Lisp 處理這種問題的方法是:讓程式設計師儘可能地提示編譯器。比如說,如果我們提前就能知道某個加法運算的兩個參數是定長數 (fixnums) ,那麼就可以對此進行宣告,這樣編譯器就會像 C 語言的那樣爲我們生成一個固定的整數加法運算。 因爲顯式型別也可以通過宣告型別來生成高效的程式碼,所以強型別和顯式型別兩種方式之間的差別並不在於運行速度。真正的區別是,在強型別語言中,型別宣告是強制性的,而顯式型別則不強加這樣的要求。 在 Common Lisp 中,型別宣告完全是可選的。它們可以讓程式運行的更快,但(除非錯誤)不會改變程式的行爲。 全局宣告以 (declaim (type fixnum *count*))
在 ANSI Common Lisp 中,可以省略 (declaim (fixnum *count*))
區域宣告通過 (defun poly (a b x)
(declare (fixnum a b x))
(+ (* a (expt x 2)) (* b x)))
在型別宣告中的變數名指的就是該宣告所在的上下文中的那個變數 ── 那個通過賦值可以改變它的值的變數。 你也可以通過 (defun poly (a b x)
(declare (fixnum a b x))
(the fixnum (+ (the fixnum (* a (the fixnum (expt x 2))))
(the fixnum (* b x)))))
看起來是不是很笨拙啊?幸運的是有兩個原因讓你很少會這樣使用 Common Lisp 中有相當多的型別 ── 恐怕有無數種型別那麼多,如果考慮到你可以自己定義新的型別的話。 型別宣告只在少數情況下至關重要,可以遵照以下兩條規則來進行:
型別宣告對內容複雜的物件特別重要,這包括陣列、結構和物件實體。這些宣告可以在兩個方面提升效率:除了可以讓編譯器來決定函數參數的型別以外,它們也使得這些物件可以在記憶體中更高效地表示。 如果對陣列元素的型別一無所知的話,這些元素在記憶體中就不得不用一塊指標來表示。但假如預先就知道陣列包含的元素僅僅是 ── 比方說 ── 雙精度浮點數 (double-floats),那麼這個陣列就可以用一組實際的雙精度浮點數來表示。這樣陣列將佔用更少的空間,因爲我們不再需要額外的指標指向每一個雙精度浮點數;同時,對陣列元素的存取也將更快,因爲我們不必沿著指標去讀取和寫元素。 ![]() 圖 13.1:指定元素型別的效果 你可以通過 (setf x (vector 1.234d0 2.345d0 3.456d0)
y (make-array 3 :element-type 'double-float)
(aref y 0) 1.234d0
(aref y 1) 2.345d0
(aref y 2)3.456d0))
圖 13.1 中的每一個矩形方格代表記憶體中的一個字 (a word of memory)。這兩個陣列都由未特別指明長度的頭部 (header) 以及後續三個元素的某種表示構成。對於 注意我們使用 除了在創建陣列時指定元素的型別,你還應該在使用陣列的程式碼中,宣告陣列的維度以及它的元素型別。一個完整的向量宣告如下: (declare (type (vector fixnum 20) v))
以上程式碼宣告了一個僅含有定長數,並且長度固定爲 (setf a (make-array '(1000 1000)
:element-type 'single-float
:initial-element 1.0s0))
(defun sum-elts (a)
(declare (type (simple-array single-float (1000 1000))
a))
(let ((sum 0.0s0))
(declare (type single-float sum))
(dotimes (r 1000)
(dotimes (c 1000)
(incf sum (aref a r c))))
sum))
圖 13.2 對陣列元素求和 最爲通用的陣列宣告形式由陣列型別以及緊接其後的元素型別和一個維度列表構成: (declare (type (simple-array fixnum (4 4)) ar))
圖 13.2 示範了如何創建一個 1000×1000 的單精度浮點數陣列,以及如何編寫一個將該陣列元素相加的函數。陣列以列主序 (row-major order)存儲,遍歷時也應儘可能以此序進行。 我們將用 程式碼的編譯參數編譯 > (time (sum-elts a))
User Run Time = 0.43 seconds
1000000.0
如果我們把 sum-elts 中的型別宣告去掉並重新編譯它,同樣的計算將花費超過5秒的時間: > (time (sum-elts a))
User Run Time = 5.17 seconds
1000000.0
型別宣告的重要性 ── 特別是對陣列和數來說 ── 怎麼強調都不過分。上面的例子中,僅僅兩行程式碼,就可以讓 13.4 避免垃圾 (Garbage Avoidance)¶Lisp 除了可以讓你推遲考慮變數的型別以外,它還允許你推遲對記憶體分配的考慮。在程式的早期階段,暫時忽略記憶體分配和臭蟲等問題,將有助於解放你的想象力。等到程式基本固定下來以後,就可以開始考慮怎麼減少動態分配,從而讓程式運行得更快。 但是,並不是構造(consing)用得少的程式就一定快。多數 Lisp 實現一直使用著差勁的垃圾回收器,在這些實現中,過多的記憶體分配容易讓程式運行變得緩慢。因此,『高效的程式應該儘可能地減少 本節介紹了幾種方法,用於減少程式中的構造。但構造數量的減少是否有利於加快程式的運行,這一點最終還是取決於實現。最好的辦法就是自己去試一試。 減少構造的辦法有很多種。有些辦法對程式的修改非常少。 例如,最簡單的方法就是使用破壞性函數。下表羅列了一些常用的函數,以及這些函數對應的破壞性版本。
當確認修改列表是安全的時候,可以使用 即便你想完全擺脫構造,你也不必放棄在執行時創建物件的可能性。你需要做的是避免在運行中爲它們分配空間和通過垃圾回收收回空間。通用方案是你自己預先分配記憶體塊 (block of memory),以及明確回收用過的塊。預先可能意味著在編譯期或者某些初始化例程中。具體情況還應具體分析。 例如,當情況允許我們利用一個有限大小的堆棧時,我們可以讓堆棧在一個已經分配了空間的向量中增長或縮減,而不是構造它。Common Lisp 內建支持把向量作爲堆棧使用。如果我們傳給 > (setf *print-array* t)
T
> (setf vec (make-array 10 :fill-pointer 2
:initial-element nil))
#(NIL NIL)
我們剛剛創建的向量對於操作序列的函數來說,仍好像只含有兩個元素, > (length vec)
2
但它能夠增長直到十個元素。因爲 > (vector-push 'a vec)
2
> vec
#(NIL NIL A)
> (vector-pop vec)
A
> vec
#(NIL NIL)
當我們呼叫 使用帶有填充指標的向量有一個缺點,就是它們不再是簡單向量了。我們不得不使用 (defconstant dict (make-array 25000 :fill-pointer 0))
(defun read-words (from)
(setf (fill-pointer dict) 0)
(with-open-file (in from :direction :input)
(do ((w (read-line in nil :eof)
(read-line in nil :eof)))
((eql w :eof))
(vector-push w dict))))
(defun xform (fn seq) (map-into seq fn seq))
(defun write-words (to)
(with-open-file (out to :direction :output
:if-exists :supersede)
(map nil #'(lambda (x)
(fresh-line out)
(princ x out))
(xform #'nreverse
(sort (xform #'nreverse dict)
#'string<)))))
圖 13.3 生成同韻字辭典 當應用涉及很長的序列時,你可以用 (setf v (map-into v #'1+ v))
圖 13.3 展示了一個使用大向量應用的例子:一個生成簡單的同韻字辭典 (或者更確切的說,一個不完全韻辭典)的程式。函數 a amoeba alba samba marimba...
結束是 ...megahertz gigahertz jazz buzz fuzz
利用填充指標和 在數值應用中要當心大數 (bignums)。大數運算需要構造,因此也就會比較慢。即使程式的最後結果爲大數,但是,通過調整計算,將中間結果保存在定長數中,這種優化也是有可能的。 另一個避免垃圾回收的方法是,鼓勵編譯器在棧上分配物件而不是在堆上。如果你知道只是臨時需要某個東西,你可以通過將它宣告爲 通過一個動態範圍 (dynamic extent)變數宣告,你告訴編譯器,變數的值應該和變數保持相同的生命期。 什麼時候值的生命期比變數長呢?這裡有個例子: (defun our-reverse (lst)
(let ((rev nil))
(dolist (x lst)
(push x rev))
rev))
在 相比之下,考慮如下 (defun our-adjoin (obj lst &rest args)
(if (apply #'member obj lst args)
lst
(cons obj lst)))
在這個例子裡,我們可以從函數的定義看出, (defun our-adjoin (obj lst &rest args)
(declare (dynamic-extent args))
(if (apply #'member obj lst args)
lst
(cons obj lst)))
那麼編譯器就可以 (但不是必須)在棧上爲 13.5 範例: 存儲池 (Example: Pools)¶對於涉及資料結構的應用,你可以通過在一個存儲池 (pool)中預先分配一定數量的結構來避免動態分配。當你需要一個結構時,你從池中取得一份,當你用完後,再把它送回池中。爲了示範存儲池的使用,我們將快速的編寫一段記錄港口中船舶數量的程式原型 (prototype of a program),然後用存儲池的方式重寫它。 (defparameter *harbor* nil)
(defstruct ship
name flag tons)
(defun enter (n f d)
(push (make-ship :name n :flag f :tons d)
*harbor*))
(defun find-ship (n)
(find n *harbor* :key #'ship-name))
(defun leave (n)
(setf *harbor*
(delete (find-ship n) *harbor*)))
圖 13.4 港口 圖 13.4 中展示的是第一個版本。 全局變數 一個程式的初始版本這麼寫簡直是棒呆了,但它會產生許多的垃圾。當這個程式運行時,它會在兩個方面構造:當船只進入港口時,新的結構將會被分配;而 我們可以通過在編譯期分配空間來消除這兩種構造的源頭 (sources of consing)。圖 13.5 展示了程式的第二個版本,它根本不會構造。 (defconstant pool (make-array 1000 :fill-pointer t))
(dotimes (i 1000)
(setf (aref pool i) (make-ship)))
(defconstant harbor (make-hash-table :size 1100
:test #'eq))
(defun enter (n f d)
(let ((s (if (plusp (length pool))
(vector-pop pool)
(make-ship))))
(setf (ship-name s) n
(ship-flag s) f
(ship-tons s) d
(gethash n harbor) s)))
(defun find-ship (n) (gethash n harbor))
(defun leave (n)
(let ((s (gethash n harbor)))
(remhash n harbor)
(vector-push s pool)))
圖 13.5 港口(第二版) 嚴格說來,新的版本仍然會構造,只是不在運行期。在第二個版本中, 我們使用存儲池的行爲實際上是肩負起記憶體管理的工作。這是否會讓我們的程式更快仍取決於我們的 Lisp 實現怎樣管理記憶體。總的說來,只有在那些仍使用著原始垃圾回收器的實現中,或者在那些對 GC 的不可預見性比較敏感的實時應用中才值得一試。 13.6 快速運算子 (Fast Operators)¶本章一開始就宣稱 Lisp 是兩種不同的語言。就某種意義來講這確實是正確的。如果你仔細看過 Common Lisp 的設計,你會發現某些特性主要是爲了速度,而另外一些主要爲了便捷性。 例如,你可以通過三個不同的函數取得向量給定位置上的元素: 對於列表來說,你應該呼叫 另一對相似的函數是 使用 當被調函數有一個剩餘參數時,呼叫 (apply #'+ '(1 2 3))
寫成如下可以更高效: (reduce #'+ '(1 2 3))
它不僅有助於呼叫正確的函數,還有助於按照正確的方式呼叫它們。餘留、可選和關鍵字參數 是昂貴的。只使用普通參數,函數呼叫中的參量會被呼叫者簡單的留在被調者能夠找到的地方。但其他種類的參數涉及運行時的處理。關鍵字參數是最差的。針對內建函數,優秀的編譯器採用特殊的辦法把使用關鍵字參量的呼叫編譯成快速程式碼 (fast code)。但對於你自己編寫的函數,避免在程式中對速度敏感的部分使用它們只有好處沒有壞處。另外,不把大量的參量都放到餘留參數中也是明智的舉措,如果這可以避免的話。 不同的編譯器有時也會有一些它們獨門優化。例如,有些編譯器可以針對鍵值是一個狹小範圍中的整數的 13.7 二階段開發 (Two-Phase Development)¶在以速度至上的應用中,你也許想要使用諸如 C 或者彙編這樣的低級語言來重寫一個 Lisp 程式的某部分。你可以對用任何語言編寫的程式使用這一技巧 ── C 程式的關鍵部分經常用彙編重寫 ── 但語言越抽象,用兩階段(two phases)開發程式的好處就越明顯。 Common Lisp 沒有規定如何集成其他語言所編寫的程式碼。這部分留給了實現決定,而幾乎所有的實現都提供了某種方式來實現它。 使用一種語言編寫程式然後用另一種語言重寫它其中部分看起來可能是一種浪費。事實上,經驗顯示這是一種好的開發軟體的方式。先針對功能、然後是速度比試著同時達成兩者來的簡單。 如果編程完全是一個機械的過程 ── 簡單的把規格說明翻譯爲程式碼 ── 在一步中把所有的事情都搞定也許是合理的。但編程永遠不是如此。不論規格說明多麼精確, 編程總是涉及一定量的探索 ── 通常比任何人能預期到的還多的多。 一份好的規格說明,也許會讓編程看起來像是簡單的把它們翻譯成程式碼的過程。這是一個普遍的誤區。編程必定涉及探索,因爲規格說明必定含糊不清。如果它們不含糊的話,它們就都算不上規格說明。 在其他領域,儘可能精準的規格說明也許是可取的。如果你要求一塊金屬被切割成某種形狀,最好準確的說出你想要的。但這個規則不適用於軟體,因爲程式和規格說明由相同的東西構成:文字。你不可能編寫出完全合意的規格說明。如果規格說明有那麼精確的話,它們就變成程式了。 λ 對於存在著可觀數量的探索的應用(再一次,比任何人承認的還要多,將實現分成兩個階段是值得的。而且在第一階段中你所使用的手段 (medium)不必就是最後的那個。例如,製作銅像的標準方法是先從粘土開始。你先用粘土做一個塑像出來,然後用它做一個模子,在這個模子中鑄造銅像。在最後的塑像中是沒有丁點粘土的,但你可以從銅像的形狀中認識到它發揮的作用。試想下從一開始就只用一塊兒銅和一個鑿子來製造這麼個一模一樣的塑像要多難啊!出於相同的原因,首先用 Lisp 來編寫程式,然後用 C 改寫它,要比從頭開始就用 C 編寫這個程式要好。 Chapter 13 總結 (Summary)¶
Chapter 13 練習 (Exercises)¶
(defun foo (x)
(if (zerop x)
0
(1+ (foo (1- x)))))
注意:你需要增加額外的參數。
(a) 在 5.7 節中的日期運算程式碼。
(b) 在 9.8 節中的光線跟蹤器 (ray-tracer)。
腳註
第十四章:進階議題¶本章是選擇性閱讀的。本章描述了 Common Lisp 裡一些更深奧的特性。Common Lisp 像是一個冰山:大部分的功能對於那些永遠不需要他們的多數用戶是看不見的。你或許永遠不需要自己定義包 (Package)或讀取宏 (read-macros),但當你需要時,有些例子可以讓你參考是很有用的。 14.1 型別標識符 (Type Specifiers)¶型別在 Common Lisp 裡不是物件。舉例來說,沒有物件對應到 一個型別標識符是一個型別的名稱。最簡單的型別標識符是像是 一個型別實際上只是一個物件集合。這意味著有多少型別就有多少個物件的集合:一個無窮大的數目。我們可以用原子的型別標識符 (atomic type specifiers)來表示某些集合:比如 舉例來說,如果 如果 (or vector (and list (not (satisfies circular?))))
某些原子的型別標識符也可以出現在複合型別標識符。要表示介於 1 至 100 的整數(包含),我們可以用: (integer 1 100)
這樣的型別標識符用來表示一個有限的型別 (finite type)。 在一個複合型別標識符裡,你可以通過在一個參數的位置使用 (simple-array fixnum (* *))
描述了指定給 (simple-array fixnum *)
描述了指定給 (simple-array fixnum)
若一個複合型別標識符沒有傳入參數,你可以使用一個原子。所以 如果有某些複合型別標識符你想重複使用,你可以使用 (deftype proseq ()
'(or vector (and list (not (satisfies circular?)))))
我們定義了 > (typep #(1 2) 'proseq)
T
如果你定義一個接受參數的型別標識符,參數會被視爲 Lisp 形式(即沒有被求值),與 (deftype multiple-of (n)
`(and integer (satisfies (lambda (x)
(zerop (mod x ,n))))))
(譯註: 注意上面 程式碼是使用反引號 定義了 (multiple-of n) 當成所有 > (type 12 '(multiple-of 4))
T
型別標識符會被直譯 (interpreted),因此很慢,所以通常你最好定義一個函數來處理這類的測試。 14.2 二進制流 (Binary Streams)¶第 7 章曾提及的流有二進制流 (binary streams)以及字元流 (character streams)。一個二進制流是一個整數的來源及/或終點,而不是字元。你通過指定一個整數的子型別來創建一個二進制流 ── 當你打開流時,通常是用 關於二進制流的 I/O 函數僅有兩個, (defun copy-file (from to)
(with-open-file (in from :direction :input
:element-type 'unsigned-byte)
(with-open-file (out to :direction :output
:element-type 'unsigned-byte)
(do ((i (read-byte in nil -1)
(read-byte in nil -1)))
((minusp i))
(declare (fixnum i))
(write-byte i out)))))
僅通過指定 (unsigned-byte 7)
來傳給 14.3 讀取宏 (Read-Macros)¶7.5 節介紹過宏字元 (macro character)的概念,一個對於 函數 Lisp 中最古老的讀取宏之一是 (set-macro-character #\'
#'(lambda (stream char)
(list (quote quote) (read stream t nil t))))
當 譯註: 現在我們明白了 (譯註:困惑的話可以看看 read 的定義 ) 你可以(通過使用 你可以通過呼叫 程式碼定義了 (set-dispatch-macro-character #\# #\?
#'(lambda (stream char1 char2)
(list 'quote
(let ((lst nil))
(dotimes (i (+ (read stream t nil t) 1))
(push i lst))
(nreverse lst)))))
現在 > #?7
(1 2 3 4 5 6 7)
除了簡單的宏字元,最常定義的宏字元是列表分隔符 (list delimiters)。另一個保留給用戶的字元組是 (set-macro-character #\} (get-macro-character #\)))
(set-dispatch-macro-character #\# #\{
#'(lambda (stream char1 char2)
(let ((accum nil)
(pair (read-delimited-list #\} stream t)))
(do ((i (car pair) (+ i 1)))
((> i (cadr pair))
(list 'quote (nreverse accum)))
(push i accum)))))
這定義了一個這樣形式 > #{2 7}
(2 3 4 4 5 6 7)
函數 如果你想要在定義一個讀取宏的檔案裡使用該讀取宏,則讀取宏的定義應要包在一個 14.4 包 (Packages)¶一個包是一個將名字映對到符號的 Lisp 物件。當前的包總是存在全局變數 > (package-name *package*)
"COMMON-LISP-USER"
> (find-package "COMMON-LISP-USER")
#<Package "COMMON-LISP-USER" 4CD15E>
通常一個符號在讀入時就被 interned 至當前的包裡面了。函數 (symbol-package 'sym)
#<Package "COMMON-LISP-USER" 4CD15E>
有趣的是,這個表達式返回它該返回的值,因爲表達式在可以被求值前必須先被讀入,而讀取這個表達式導致 > (setf sym 99)
99
現在我們可以創建及切換至一個新的包: > (setf *package* (make-package 'mine
:use '(common-lisp)))
#<Package "MINE" 63390E>
現在應該會聽到詭異的背景音樂,因爲我們來到一個不一樣的世界了:
在這裡 MINE> sym
Error: SYM has no value
爲什麼會這樣?因爲上面我們設爲 99 的 MINE> common-lisp-user::sym
99
所以有著相同打印名稱的不同符號能夠在不同的包內共存。可以有一個 包也提供了資訊隱藏的手段。程式應通過函數與變數的名字來參照它們。如果你不讓一個名字在你的包之外可見的話,那麼另一個包中的 程式碼就無法使用或者修改這個名字所參照的物件。 通常使用兩個冒號作爲包的前綴也是很差的風格。這麼做你就違反了包本應提供的模組性。如果你不得不使用一個雙冒號來參照到一個符號,這是因爲某人根本不想讓你用。 通常我們應該只參照被輸出 ( exported )的符號。如果我們回到用戶包裡,並輸出一個被 interned 的符號, MINE> (in-package common-lisp-user)
#<Package "COMMON-LISP-USER" 4CD15E>
> (export 'bar)
T
> (setf bar 5)
5
我們使這個符號對於其它的包是可視的。現在當我們回到 > (in-package mine)
#<Package "MINE" 63390E>
MINE> common-lisp-user:bar
5
通過把 MINE> (import 'common-lisp-user:bar)
T
MINE> bar
5
在輸入 要是已經有一個了怎麼辦?在這種情況下, MINE> (import 'common-lisp-user::sym)
Error: SYM is already present in MINE.
在此之前,當我們試著在 另一個方法來獲得別的包內符號的存取權是使用( MINE> (use-package 'common-lisp-user)
T
現在所有由用戶包 (譯註: common-lisp-user 包)所輸出的符號,可以不需要使用任何限定符在 含有自帶運算子及變數名字的包叫做 MINE> #'cons
#<Compiled-Function CONS 462A3E>
在編譯後的 程式碼中, 通常不會像這樣在頂層進行包的操作。更常見的是包的呼叫會包含在源檔案裡。通常,只要把 這種由包所提供的模組性實際上有點奇怪。我們不是物件的模組 (modules),而是名字的模組。 每一個使用了 14.5 Loop 宏 (The Loop Facility)¶
程式碼。與其撰寫 Lisp 程式碼,你用一種更接近英語的形式來表達你的程式,然後這個形式被翻譯成 Lisp。不幸的是, 如果你是曾經計劃某天要理解 這個宏唯一的實際定義是它的實現方式,而唯一可以理解它(如果有人可以理解的話)的方法是通過實體。ANSI 標準討論 第一個關於 一個 程式碼。這些階段如下:
我們會看幾個 程式碼會貢獻至何個階段。 舉例來說,最簡單的 程式碼: > (loop for x from 0 to 9
do (princ x))
0123456789
NIL
這個
貢獻 程式碼至前兩個階段,導致
貢獻 程式碼給主體。 一個更通用的 > (loop for x = 8 then (/ x 2)
until (< x 1)
do (princ x))
8421
NIL
你可以使用 > (loop for x from 1 to 4
and y from 1 to 4
do (princ (list x y)))
(1 1)(2 2)(3 3)(4 4)
NIL
要不然有多重 另一件在迭代 程式碼通常會做的事是累積某種值。舉例來說: > (loop for x in '(1 2 3 4)
collect (1+ x))
(2 3 4 5)
在 在這個情況裡, 程式碼至三個階段。在序幕,一個匿名累加器 (anonymous accumulator)設為 這是返回一個特定值的第一個例子。有用來明確指定返回值的子句,但沒有這些子句時,一個
> (loop for x from 1 to 5
collect (random 10))
(3 8 6 5 0)
這裡我們獲得了一個含五個隨機數的列表。這跟我們定義過的 一個 (defun even/odd (ns)
(loop for n in ns
if (evenp n)
collect n into evens
else collect n into odds
finally (return (values evens odds))))
一個 程式碼至閉幕。在這個情況它指定了返回值。 一個 (defun sum (n)
(loop for x from 1 to n
sum x))
(defun most (fn lst)
(if (null lst)
(values nil nil)
(let* ((wins (car lst))
(max (funcall fn wins)))
(dolist (obj (cdr lst))
(let ((score (funcall fn obj)))
(when (> score max)
(setf wins obj
max score))))
(values wins max))))
(defun num-year (n)
(if (< n 0)
(do* ((y (- yzero 1) (- y 1))
(d (- (year-days y)) (- d (year-days y))))
((<= d n) (values y (- n d))))
(do* ((y yzero (+ y 1))
(prev 0 d)
(d (year-days y) (+ d (year-days y))))
((> d n) (values y (- n prev))))))
圖 14.1 不使用 loop 的迭代函數 (defun most (fn lst)
(if (null lst)
(values nil nil)
(loop with wins = (car lst)
with max = (funcall fn wins)
for obj in (cdr lst)
for score = (funcall fn obj)
when (> score max)
(do (setf wins obj
max score)
finally (return (values wins max))))))
(defun num-year (n)
(if (< n 0)
(loop for y downfrom (- yzero 1)
until (<= d n)
sum (- (year-days y)) into d
finally (return (values (+ y 1) (- n d))))
(loop with prev = 0
for y from yzero
until (> d n)
do (setf prev d)
sum (year-days y) into d
finally (return (values (- y 1)
(- n prev))))))
圖 14.2 使用 loop 的迭代函數 一個 (loop for y = 0 then z
for x from 1 to 5
sum 1 into z
finally (return y z))
(loop for x from 1 to 5
for y = 0 then z
sum 1 into z
finally (return y z))
它們看起來夠簡單 ── 每一個有四個子句。但它們返回同樣的值嗎?它們返回的值多少?你若試著在標準中想找答案將徒勞無功。每一個 由於這類原因,使用 程式碼看起來更容易理解。 14.6 狀況 (Conditions)¶在 Common Lisp 裡,狀況 (condition)包括了錯誤以及其它可能在執行期發生的情況。當一個狀況被捕捉時 (signalled),相應的處理程式 (handler)會被呼叫。處理錯誤狀況的預設處理程式通常會呼叫一個中斷迴圈 (break-loop)。但 Common Lisp 提供了多樣的運算子來捕捉及處理錯誤。要覆寫預設的處理程式,甚至是自己寫一個新的處理程式也是有可能的。 多數的程式設計師不會直接處理狀況。然而有許多更抽象的運算子使用了狀況,而要了解這些運算子,知道背後的原理是很有用的。 Common lisp 有數個運算子用來捕捉錯誤。最基本的是 > (error "Your report uses ~A as a verb." 'status)
Error: Your report uses STATUS as a verb
Options: :abort, :backtrace
>>
如上所示,除非這樣的狀況被處理好了,不然執行就會被打斷。 用來捕捉錯誤的更抽象運算子包括了 > (ecase 1 (2 3) (4 5))
Error: No applicable clause
Options: :abort, :backtrace
>>
普通的
> (let ((x '(a b c)))
(check-type (car x) integer "an integer")
x)
Error: The value of (CAR X), A, should be an integer.
Options: :abort, :backtrace, :continue
>> :continue
New value of (CAR X)? 99
(99 B C)
>
在這個例子裡, 這個宏是用更通用的 > (let ((sandwich '(ham on rye)))
(assert (eql (car sandwich) 'chicken)
((car sandwich))
"I wanted a ~A sandwich." 'chicken)
sandwich)
Error: I wanted a CHICKEN sandwich.
Options: :abort, :backtrace, :continue
>> :continue
New value of (CAR SANDWICH)? 'chicken
(CHICKEN ON RYE)
要建立新的處理程式也是可能的,但大多數程式設計師只會間接的利用這個可能性,通過使用像是 舉例來說,如果在某個時候,你想要用戶能夠輸入一個表達式,但你不想要在輸入是語法上不合時中斷執行,你可以這樣寫: (defun user-input (prompt)
(format t prompt)
(let ((str (read-line)))
(or (ignore-errors (read-from-string str))
nil)))
若輸入包含語法錯誤時,這個函數僅返回 > (user-input "Please type an expression")
Please type an expression> #%@#+!!
NIL
腳註
第十五章:範例:推論¶接下來三章提供了大量的 Lisp 程式例子。選擇這些例子來說明那些較長的程式所採取的形式,和 Lisp 所擅長解決的問題型別。 在這一章中我們將要寫一個基於一組 15.1 目標 (The Aim)¶在這個程式中,我們將用一種熟悉的形式來表示資訊:包含單個判斷式,以及跟在之後的零個或多個參數所組成的列表。要表示 Donald 是 Nancy 的家長,我們可以這樣寫: (parent donald nancy)
事實上,我們的程式是要表示一些從已有的事實作出推斷的規則。我們可以這樣來表示規則: (<- head body)
其中, (<- (child ?x ?y) (parent ?y ?x))
表示:如果 y 是 x 的家長,那麼 x 是 y 的孩子;更恰當地說,我們可以通過證明 可以把規則中的 body 部分(if-part) 寫成一個複雜的表達式,其中包含 (<- (father ?x ?y) (and (parent ?x ?y) (male ?x)))
一些規則可能依賴另一些規則所產生的事實。比如,我們寫的第一個規則是爲了證明 (<- (daughter ?x ?y) (and (child ?x ?y) (female ?x)))
然後使用它來證明 表達式的證明可以回溯任意數量的規則,只要它最終結束於給出的已知事實。這個過程有時候被稱爲反向連結 (backward-chaining)。之所以說 反向 (backward) 是因爲這一類推論先考慮 head 部分,這是爲了在繼續證明 body 部分之前檢查規則是否有效。連結 (chaining) 來源於規則之間的依賴關係,從我們想要證明的內容到我們的已知條件組成一個連結 (儘管事實上它更像一棵樹)。 λ 15.2 匹配 (Matching)¶我們需要有一個函數來做模式匹配以完成我們的反向連結 (back-chaining) 程式,這個函數能夠比較兩個包含變數的列表,它會檢查在給變數賦值後是否可以使兩個列表相等。舉例,如果 (p ?x ?y c ?x)
(p a b c a)
當 (p ?x b ?y a)
(p ?y b c a)
當 我們有一個 (defun match (x y &optional binds)
(cond
((eql x y) (values binds t))
((assoc x binds) (match (binding x binds) y binds))
((assoc y binds) (match x (binding y binds) binds))
((var? x) (values (cons (cons x y) binds) t))
((var? y) (values (cons (cons y x) binds) t))
(t
(when (and (consp x) (consp y))
(multiple-value-bind (b2 yes)
(match (car x) (car y) binds)
(and yes (match (cdr x) (cdr y) b2)))))))
(defun var? (x)
(and (symbolp x)
(eql (char (symbol-name x) 0) #\?)))
(defun binding (x binds)
(let ((b (assoc x binds)))
(if b
(or (binding (cdr b) binds)
(cdr b)))))
圖 15.1: 匹配函數。 > (match '(p a b c a) '(p ?x ?y c ?x))
((?Y . B) (?X . A))
T
> (match '(p ?x b ?y a) '(p ?y b c a))
((?Y . C) (?X . ?Y))
T
> (match '(a b c) '(a a a))
NIL
當 > (match '(p ?x) '(p ?x))
NIL
T
如果
下面是一個例子,按順序來說明以上六種情況: > (match '(p ?v b ?x d (?z ?z))
'(p a ?w c ?y ( e e))
'((?v . a) (?w . b)))
((?Z . E) (?Y . D) (?X . C) (?V . A) (?W . B))
T
> (match '(?x a) '(?y ?y))
((?Y . A) (?X . ?Y))
T
先匹配 15.3 回答查詢 (Answering Queries)¶在介紹了綁定的概念之後,我們可以更準確的說一下我們的程式將要做什麼:它得到一個可能包含變數的表達式,根據我們給定的事實和規則返回使它正確的所有綁定。比如,我們只有下面這個事實: (parent donald nancy)
然後我們想讓程式證明: (parent ?x ?y)
它會返回像下面這樣的表達: (((?x . donald) (?y . nancy)))
它告訴我們只有一個可以讓這個表達式爲真的方法: 在通往目標的路上,我們已經有了一個的重要部分:一個匹配函數。 下面是用來定義規則的一段 程式碼: (defvar *rules* (make-hash-table))
(defmacro <- (con &optional ant)
`(length (push (cons (cdr ',con) ',ant)
(gethash (car ',con) *rules*))))
圖 15.2 定義規則 規則將被包含於一個叫做 我們將要使用同一個宏 > (<- (parent donald nancy))
1
> (<- (child ?x ?y) (parent ?y ?x))
1
呼叫 下面是我們的推論程式所需的大多數 程式碼: (defun prove (expr &optional binds)
(case (car expr)
(and (prove-and (reverse (cdr expr)) binds))
(or (prove-or (cdr expr) binds))
(not (prove-not (cadr expr) binds))
(t (prove-simple (car expr) (cdr expr) binds))))
(defun prove-simple (pred args binds)
(mapcan #'(lambda (r)
(multiple-value-bind (b2 yes)
(match args (car r)
binds)
(when yes
(if (cdr r)
(prove (cdr r) b2)
(list b2)))))
(mapcar #'change-vars
(gethash pred *rules*))))
(defun change-vars (r)
(sublis (mapcar #'(lambda (v) (cons v (gensym "?")))
(vars-in r))
r))
(defun vars-in (expr)
(if (atom expr)
(if (var? expr) (list expr))
(union (vars-in (car expr))
(vars-in (cdr expr)))))
圖 15.3: 推論。 上面 程式碼中的 > (prove-simple 'parent '(donald nancy) nil)
(NIL)
> (prove-simple 'child '(?x ?y) nil)
(((#:?6 . NANCY) (#:?5 . DONALD) (?Y . #:?5) (?X . #:?6)))
以上兩個返回值指出有一種方法可以證明我們的問題。(一個失敗的證明將返回 nil。)第一個例子產生了一組空的綁定,第二個例子產生了這樣的綁定: 順便說一句,這是一個很好的例子來實踐 2.13 節提出的觀點。因爲我們用函數式的風格來寫這個程式,所以可以交互式地測試每一個函數。 第二個例子返回的值裡那些 gensyms 是怎麼回事?如果我們打算使用含有變數的規則,我們需要避免兩個規則恰好包含相同的變數。如果我們定義如下兩條規則: (<- (child ?x ?y) (parent ?y ?x))
(<- (daughter ?y ?x) (and (child ?y ?x) (female ?y)))
第一條規則要表達的意思是:對於任何的 如果我們使用上面所寫的規則,它們將不會按預期的方式工作。如果我們嘗試證明“ a 是 b 的女兒”,匹配到第二條規則的 head 部分時會將 > (match '(child ?y ?x)
'(child ?x ?y)
'((?y . a) (?x . b)))
NIL
爲了保證一條規則中的變數只表示規則中各參數之間的關係,我們用 gensyms 來代替規則中的所有變數。這就是 現在只剩下定義用以證明複雜表達式的函數了。下面就是需要的函數: (defun prove-and (clauses binds)
(if (null clauses)
(list binds)
(mapcan #'(lambda (b)
(prove (car clauses) b))
(prove-and (cdr clauses) binds))))
(defun prove-or (clauses binds)
(mapcan #'(lambda (c) (prove c binds))
clauses))
(defun prove-not (clause binds)
(unless (prove clause binds)
(list binds)))
圖 15.4 邏輯運算子 (Logical operators) 操作一個
現在我們有了一個可以工作的程式,但它不是很友好。必須要解析 (defmacro with-answer (query &body body)
(let ((binds (gensym)))
`(dolist (,binds (prove ',query))
(let ,(mapcar #'(lambda (v)
`(,v (binding ',v ,binds)))
(vars-in query))
,@body))))
圖 15.5 介面宏 (Interface macro) 它接受一個 > (with-answer (parent ?x ?y)
(format t "~A is the parent of ~A.~%" ?x ?y))
DONALD is the parent of NANCY.
NIL
這個宏幫我們做了解析綁定的工作,同時爲我們在程式中使用 (with-answer (p ?x ?y)
(f ?x ?y))
;;將被展開成下面的
程式碼
圖 15.6: with-answer 呼叫的展開式 下面是使用它的一個例子: (<- (parent donald nancy))
(<- (parent donald debbie))
(<- (male donald))
(<- (father ?x ?y) (and (parent ?x ?y) (male ?x)))
(<- (= ?x ?y))
(<- (sibling ?x ?y) (and (parent ?z ?x)
(parent ?z ?y)
(not (= ?x ?y))))
;;我們可以像下面這樣做出推論
> (with-answer (father ?x ?y)
(format t "~A is the father of ~A.~%" ?x ?y))
DONALD is the father of DEBBIE.
DONALD is the father of NANCY.
NIL
> (with-answer (sibling ?x ?y))
(format t "~A is the sibling of ~A.~%" ?x ?y))
DEBBLE is the sibling of NANCY.
NANCY is the sibling of DEBBIE.
NIL
圖 15.7: 使用中的程式 15.4 分析 (Analysis)¶看上去,我們在這一章中寫的 程式碼,是用簡單自然的方式去實現這樣一個程式。事實上,它的效率非常差。我們在這裡是其實是做了一個解釋器。我們能夠把這個程式做得像一個編譯器。 這裡做一個簡單的描述。基本的思想是把整個程式打包到兩個宏 程式碼。當一個規則被定義的時候就有表達式可用。爲什麼要等到使用的時候才去分析它呢?這同樣適用於和 聽上去好像比我們已經寫的這個程式複雜很多,但其實可能只是長了兩三倍。想要學習這種技術的讀者可以看 On Lisp 或者 Paradigms of Artificial Intelligence Programming ,這兩本書有一些使用這種風格寫的範例程式。 第十六章:範例:產生 HTML¶本章的目標是完成一個簡單的 HTML 產生器 —— 這個程式可以自動生成一系列包含超文字連結的網頁。除了介紹特定 Lisp 技術之外,本章還是一個典型的自底向上編程(bottom-up programming)的例子。 我們以一些通用 HTML 實用函數作爲開始,繼而將這些例程看作是一門編程語言,從而更好地編寫這個產生器。 16.1 超文字標記語言 (HTML)¶HTML (HyperText Markup Language,超文字標記語言)用於構建網頁,是一種簡單、易學的語言。本節就對這種語言作概括性介紹。 當你使用網頁瀏覽器閱覽網頁時,瀏覽器從遠程服務器獲取 HTML 檔案,並將它們顯示在你的屏幕上。每個 HTML 檔案都包含任意多個標籤(tag),這些標籤相當於發送給瀏覽器的指令。 ![]() 圖 16.1 一個 HTML 檔案 圖 16.1 給出了一個簡單的 HTML 檔案,圖 16.2 展示了這個 HTML 檔案在瀏覽器裡顯示時大概是什麼樣子。 ![]() 圖 16.2 一個網頁 注意在三角符號之間的文字並沒有被顯示出來,這些用三角符號包圍的文字就是標籤。 HTML 的標籤分爲兩種,一種是成雙成對地出現的: <tag>...</tag>
第一個標籤標誌著某種情景(environment)的開始,而第二個標籤標誌著這種情景的結束。
這種標籤的一個例子是 另外一些成雙成對出現的標籤包括:創建帶編號列表的 被 一個像 <a href="foo.html">
這樣的標籤,就標識了一個指向另一個 HTML 檔案的連結,其中這個 HTML 檔案和當前網頁的檔案夾相同。
當點擊這個連結時,瀏覽器就會獲取並顯示 當然,連結並不一定都要指向相同檔案夾下的 HTML 檔案,實際上,一個連結可以指向互聯網的任何一個檔案。 和成雙成對出現的標籤相反,另一種標籤沒有結束標記。
在圖 16.1 裡有一些這樣的標籤,包括:創建一個新文字行的 HTML 還有不少其他的標籤,但是本章要用到的標籤,基本都包含在圖 16.1 裡了。 16.2 HTML 實用函數 (HTML Utilities)¶(defmacro as (tag content)
`(format t "<~(~A~)>~A</~(~A~)>"
',tag ,content ',tag))
(defmacro with (tag &rest body)
`(progn
(format t "~&<~(~A~)>~%" ',tag)
,@body
(format t "~&</~(~A~)>~%" ',tag)))
(defmacro brs (&optional (n 1))
(fresh-line)
(dotimes (i n)
(princ "<br>"))
(terpri))
圖 16.3 標籤生成例程 本節會定義一些生成 HTML 的例程。
圖 16.3 包含了三個基本的、生成標籤的例程。
所有例程都將它們的輸出發送到 宏 > (as center "The Missing Lambda")
<center>The Missing Lambda</center>
NIL
> (with center
(princ "The Unbalanced Parenthesis"))
<center>
The Unbalanced Parenthesis
</center>
NIL
兩個宏都使用了 除此之外, 圖 16.3 中的最後一個例程 (defun html-file (base)
(format nil "~(~A~).html" base))
(defmacro page (name title &rest body)
(let ((ti (gensym)))
`(with-open-file (*standard-output*
(html-file ,name)
:direction :output
:if-exists :supersede)
(let ((,ti ,title))
(as title ,ti)
(with center
(as h2 (string-upcase ,ti)))
(brs 3)
,@body))))
圖 16.4 HTML 檔案生成例程 圖 16.4 包含用於生成 HTML 檔案的例程。
第一個函數根據給定的符號(symbol)返回一個檔案名。
在一個實際應用中,這個函數可能會返回指向某個特定檔案夾的路徑(path)。
目前來說,這個函數只是簡單地將 宏 6.7 小節示範了如何臨時性地綁定一個特殊變數。
在 113 頁的例子中,我們在
如果我們呼叫 (page 'paren "The Unbalanced Parenthesis"
(princ "Something in his expression told her..."))
這會產生一個名爲 <title>The Unbalanced Parenthesis</title>
<center>
<h2>THE UNBALANCED PARENTHESIS</h2>
</center>
<br><br><br>
Something in his expression told her...
除了 (defmacro with-link (dest &rest body)
`(progn
(format t "<a href=\"~A\">" (html-file ,dest))
,@body
(princ "</a>")))
(defun link-item (dest text)
(princ "<li>")
(with-link dest
(princ text)))
(defun button (dest text)
(princ "[ ")
(with-link dest
(princ text))
(format t " ]~%"))
圖 16.5 生成連結的例程 圖片 16.5 給出了用於生成連結的例程。
> (with-link 'capture
(princ "The Captured Variable"))
<a href="capture.html">The Captured Variable</a>
"</a>"
> (link-item 'bq "Backquote!")
<li><a href="bq.html">Backquote!</a>
"</a>"
最後, > (button 'help "Help")
[ <a href="help.html">Help</a> ]
NIL
16.3 迭代式實用函數 (An Iteration Utility)¶在這一節,我們先暫停一下編寫 HTML 產生器的工作,轉到編寫迭代式例程的工作上來。 你可能會問,怎樣才能知道,什麼時候應該編寫主程式,什麼時候又應該編寫子例程? 實際上,這個問題,沒有答案。 通常情況下,你總是先開始寫一個程式,然後發現需要寫一個新的例程,於是你轉而去編寫新例程,完成它,接著再回過頭去編寫原來的程式。 時間關係,要在這裡示範這個開始-完成-又再開始的過程是不太可能的,這裡只展示這個迭代式例程的最終形態,需要注意的是,這個程式的編寫並不如想象中的那麼簡單。 程式通常需要經歷多次重寫,才會變得簡單。 (defun map3 (fn lst)
(labels ((rec (curr prev next left)
(funcall fn curr prev next)
(when left
(rec (car left)
curr
(cadr left)
(cdr left)))))
(when lst
(rec (car lst) nil (cadr lst) (cdr lst)))))
圖 16.6 對樹進行迭代 圖 16.6 裡定義的新例程是 > (map3 #'(lambda (&rest args) (princ args))
'(a b c d))
(A NIL B) (B A C) (C B D) (D C NIL)
NIL
和
> (map3 #'(lambda (c p n)
(princ c)
(if n (princ " | ")))
'(a b c d))
A | B | C | D
NIL
程式設計師經常會遇到上面的這類問題,但只要花些功夫,定義一些例程來處理它們,就能爲後續工作節省不少時間。 16.4 生成頁面 (Generating Pages)¶一本書可以有任意數量的大章,每個大章又有任意數量的小節,而每個小節又有任意數量的分節,整本書的結構呈現出一棵樹的形狀。 儘管網頁使用的術語和書本不同,但多個網頁同樣可以被組織成樹狀。 本節要構建的是這樣一個程式,它生成多個網頁,這些網頁帶有以下結構: 第一頁是一個目錄,目錄中的連結指向各個節點(section)頁面。 每個節點包含一些指向項(item)的連結。 而一個項就是一個包含純文字的頁面。 除了頁面本身的連結以外,根據頁面在樹狀結構中的位置,每個頁面都會帶有前進、後退和向上的連結。 其中,前進和後退連結用於在同級(sibling)頁面中進行導航。 舉個例子,點擊一個項頁面中的前進連結時,如果這個項的同一個節點下還有下一個項,那麼就跳到這個新項的頁面裡。 另一方面,向上連結將頁面跳轉到樹形結構的上一層 —— 如果當前頁面是項頁面,那麼返回到節點頁面;如果當前頁面是節點頁面,那麼返回到目錄頁面。 最後,還會有索引頁面:這個頁面包含一系列連結,按字母順序排列所有項。 ![]() 圖 16.7 網站的結構 圖 16.7 展示了生成程式創建的頁面所形成的連結結構。 (defparameter *sections* nil)
(defstruct item
id title text)
(defstruct section
id title items)
(defmacro defitem (id title text)
`(setf ,id
(make-item :id ',id
:title ,title
:text ,text)))
(defmacro defsection (id title &rest items)
`(setf ,id
(make-section :id ',id
:title ,title
:items (list ,@items))))
(defun defsite (&rest sections)
(setf *sections* sections))
圖 16.8 定義一個網站 圖 16.8 包含定義頁面所需的資料結構。程式需要處理兩類物件:項和節點。這兩類物件的結構很相似,不過節點包含的是項的列表,而項包含的是文字塊。 節點和項兩類物件都帶有 節點和項也同時帶有 在節點裡,項的排列順序由傳給 (defconstant contents "contents")
(defconstant index "index")
(defun gen-contents (&optional (sections *sections*))
(page contents contents
(with ol
(dolist (s sections)
(link-item (section-id s) (section-title s))
(brs 2))
(link-item index (string-capitalize index)))))
(defun gen-index (&optional (sections *sections*))
(page index index
(with ol
(dolist (i (all-items sections))
(link-item (item-id i) (item-title i))
(brs 2)))))
(defun all-items (sections)
(let ((is nil))
(dolist (s sections)
(dolist (i (section-items s))
(setf is (merge 'list (list i) is #'title<))))
is))
(defun title< (x y)
(string-lessp (item-title x) (item-title y)))
圖 16.9 生成索引和目錄 圖 16.9 包含的函數用於生成索引和目錄。
常數 函數 實際程式中的對比操作通常更複雜一些。舉個例子,它們需要忽略無意義的句首詞彙,比如 (defun gen-site ()
(map3 #'gen-section *sections*)
(gen-contents)
(gen-index))
(defun gen-section (sect <sect sect>)
(page (section-id sect) (section-title sect)
(with ol
(map3 #'(lambda (item <item item>)
(link-item (item-id item)
(item-title item))
(brs 2)
(gen-item sect item <item item>))
(section-items sect)))
(brs 3)
(gen-move-buttons (if <sect (section-id <sect))
contents
(if sect> (section-id sect>)))))
(defun gen-item (sect item <item item>)
(page (item-id item) (item-title item)
(princ (item-text item))
(brs 3)
(gen-move-buttons (if <item (item-id <item))
(section-id sect)
(if item> (item-id item>)))))
(defun gen-move-buttons (back up forward)
(if back (button back "Back"))
(if up (button up "Up"))
(if forward (button forward "Forward")))
圖 16.10 生成網站、節點和項 圖 16.10 包含其餘的程式: 所有頁面的集合包括目錄、索引、各個節點以及各個項的頁面。
目錄和索引的生成由圖 16.9 中的程式完成。
節點和項由分別由生成節點頁面的 這兩個函數的開頭和結尾非常相似。
它們都接受一個物件、物件的左兄弟、物件的右兄弟作爲參數;它們都從物件的 項所包含的內容完全由用戶決定。 比如說,將 HTML 標籤作爲內容也是完全沒問題的。 項的文字當然也可以由其他程式來生成。 圖 16.11 示範了如何手工地定義一個微型網頁。 在這個例子中,列出的項都是 Fortune 餅乾公司新推出的產品。 (defitem des "Fortune Cookies: Dessert or Fraud?" "...")
(defitem case "The Case for Pessimism" "...")
(defsection position "Position Papers" des case)
(defitem luck "Distribution of Bad Luck" "...")
(defitem haz "Health Hazards of Optimism" "...")
(defsection abstract "Research Abstracts" luck haz)
(defsite position abstract)
圖 16.11 一個微型網站 第十七章:範例:物件¶在本章裡,我們將使用 Lisp 來自己實現物件導向語言。這樣子的程式稱爲嵌入式語言 (embedded language)。嵌入一個物件導向語言到 Lisp 裡是一個絕佳的例子。同時作爲一個 Lisp 的典型用途,並示範了面向物件的抽象是如何多自然地在 Lisp 基本的抽象上構建出來。 17.1 繼承 (Inheritance)¶11.10 小節解釋過通用函數與訊息傳遞的差別。 在訊息傳遞模型裡,
當然了,我們知道 CLOS 使用的是通用函數模型。但本章我們只對於寫一個迷你的物件系統 (minimal object system)感興趣,而不是一個可與 CLOS 匹敵的系統,所以我們將使用訊息傳遞模型。 我們已經在 Lisp 裡看過許多保存屬性集合的方法。一種可能的方法是使用雜湊表來代表物件,並將屬性作爲雜湊表的條目保存。接著可以通過 (gethash 'color obj)
由於函數是資料物件,我們也可以將函數作爲屬性保存起來。這表示我們也可以有方法;要呼叫一個物件特定的方法,可以通過 (funcall (gethash 'move obj) obj 10)
我們可以在這個概念上,定義一個 Smalltalk 風格的訊息傳遞語法, (defun tell (obj message &rest args)
(apply (gethash message obj) obj args))
所以想要一個物件 (tell obj 'move 10)
事實上,純 Lisp 唯一缺少的原料是繼承。我們可以通過定義一個遞迴版本的 (defun rget (prop obj)
(multiple-value-bind (val in) (gethash prop obj)
(if in
(values val in)
(let ((par (gethash :parent obj)))
(and par (rget prop par))))))
(defun tell (obj message &rest args)
(apply (rget message obj) obj args))
圖 17.1:繼承 讓我們用這段程式,來試試本來的例子。我們創建兩個物件,其中一個物件是另一個的子類: > (setf circle-class (make-hash-table)
our-circle (make-hash-table)
(gethash :parent our-circle) circle-class
(gethash 'radius our-circle) 2)
2
> (setf (gethash 'area circle-class)
#'(lambda (x)
(* pi (expt (rget 'radius x) 2))))
#<Interpreted-Function BF1EF6>
現在當我們詢問 > (rget 'radius our-circle)
2
T
> (tell our-circle 'area)
12.566370614359173
在開始改善這個程式之前,值得停下來想想我們到底做了什麼。僅使用 8 行程式碼,我們使純的、舊的、無 CLOS 的 Lisp ,轉變成一個物件導向語言。我們是怎麼完成這項壯舉的?應該用了某種祕訣,才會僅用了 8 行程式碼,就實現了物件導向程式設計。 的確有一個祕訣存在,但不是編程的奇技淫巧。這個祕訣是,Lisp 本來就是一個面向物件的語言了,甚至說,是種更通用的語言。我們需要做的事情,不過就是把本來就存在的抽象,再重新包裝一下。 17.2 多重繼承 (Multiple Inheritance)¶到目前爲止我們只有單繼承 ── 一個物件只可以有一個父類。但可以通過使 在只有單繼承的情況下,當我們想要從物件取出某些屬性,只需要遞迴地延著祖先的方嚮往上找。如果物件本身沒有我們想要屬性的有關資訊,可以檢視其父類,以此類推。有了多重繼承後,我們仍想要執行同樣的搜索,但這件簡單的事,卻被物件的祖先可形成一個圖,而不再是簡單的樹給複雜化了。不能只使用深度優先來搜索這個圖。有多個父類時,可以有如圖 17.3 所示的層級存在: 如果我們想要實現普遍的繼承概念,就不應該在檢查其子孫前,先檢查該物件。在這個情況下,適當的搜索順序會是 我們可以通過利用優先序列表的優點,舉例來說,一個愛國的無賴先是一個無賴,然後才是愛國者: > (setf scoundrel (make-hash-table)
patriot (make-hash-table)
patriotic-scoundrel (make-hash-table)
(gethash 'serves scoundrel) 'self
(gethash 'serves patriot) 'country
(gethash :parents patriotic-scoundrel)
(list scoundrel patriot))
(#<Hash-Table C41C7E> #<Hash-Table C41F0E>)
> (rget 'serves patriotic-scoundrel)
SELF
T
到目前爲止,我們有一個強大的程式,但極其醜陋且低效。在一個 Lisp 程式生命週期的第二階段,我們將這個初步框架提煉成有用的東西。 17.3 定義物件 (Defining Objects)¶第一個我們需要改善的是,寫一個用來創建物件的函數。我們程式表示物件以及其父類的方式,不需要給用戶知道。如果我們定義一個函數來創建物件,用戶將能夠一個步驟就創建出一個物件,並指定其父類。我們可以在創建一個物件的同時,順道構造優先序列表,而不是在每次當我們需要找一個屬性或方法時,才花費龐大代價來重新構造。 如果我們要維護優先序列表,而不是在要用的時候再構造它們,我們需要處理列表會過時的可能性。我們的策略會是用一個列表來保存所有存在的物件,而無論何時當某些父類被改動時,重新給所有受影響的物件生成優先序列表。這代價是相當昂貴的,但由於查詢比重定義父類的可能性來得高許多,我們會省下許多時間。這個改變對我們的程式的靈活性沒有任何影響;我們只是將花費從頻繁的操作轉到不頻繁的操作。 圖 17.4 包含了新的程式。 λ 全局的 用戶現在不用呼叫 (defvar *objs* nil)
(defun parents (obj) (gethash :parents obj))
(defun (setf parents) (val obj)
(prog1 (setf (gethash :parents obj) val)
(make-precedence obj)))
(defun make-precedence (obj)
(setf (gethash :preclist obj) (precedence obj))
(dolist (x *objs*)
(if (member obj (gethash :preclist x))
(setf (gethash :preclist x) (precedence x)))))
(defun obj (&rest parents)
(let ((obj (make-hash-table)))
(push obj *objs*)
(setf (parents obj) parents)
obj))
(defun rget (prop obj)
(dolist (c (gethash :preclist obj))
(multiple-value-bind (val in) (gethash prop c)
(if in (return (values val in))))))
圖 17.4:創建物件 17.4 函數式語法 (Functional Syntax)¶另一個可以改善的空間是消息呼叫的語法。 (tell (tell obj 'find-owner) 'find-owner)
我們可以使用圖 17.5 所定義的 (defmacro defprop (name &optional meth?)
`(progn
(defun ,name (obj &rest args)
,(if meth?
`(run-methods obj ',name args)
`(rget ',name obj)))
(defun (setf ,name) (val obj)
(setf (gethash ',name obj) val))))
(defun run-methods (obj name args)
(let ((meth (rget name obj)))
(if meth
(apply meth obj args)
(error "No ~A method for ~A." name obj))))
圖 17.5: 函數式語法 (defprop find-owner t)
我們就可以在函數呼叫裡引用它,則我們的程式讀起來將會再次回到 Lisp 本來那樣: (find-owner (find-owner obj))
我們的前一個例子在某種程度上可讀性變得更高了: > (progn
(setf scoundrel (obj)
patriot (obj)
patriotic-scoundrel (obj scoundrel patriot))
(defprop serves)
(setf (serves scoundrel) 'self
(serves patriot) 'country)
(serves patriotic-scoundrel))
SELF
T
17.5 定義方法 (Defining Methods)¶到目前爲止,我們藉由敘述如下的東西來定義一個方法: (defprop area t)
(setf circle-class (obj))
(setf (area circle-class)
#'(lambda (c) (* pi (expt (radius c) 2))))
(defmacro defmeth (name obj parms &rest body)
(let ((gobj (gensym)))
`(let ((,gobj ,obj))
(setf (gethash ',name ,gobj)
(labels ((next () (get-next ,gobj ',name)))
#'(lambda ,parms ,@body))))))
(defun get-next (obj name)
(some #'(lambda (x) (gethash name x))
(cdr (gethash :preclist obj))))
圖 17.6 定義方法。 在一個方法裡,我們可以通過給物件的 (setf grumpt-circle (obj circle-class))
(setf (area grumpt-circle)
#'(lambda (c)
(format t "How dare you stereotype me!~%")
(funcall (some #'(lambda (x) (gethash 'area x))
(cdr (gethash :preclist c)))
c)))
這裡 圖 17.6 的 (defmeth area circle-class (c)
(* pi (expt (radius c) 2)))
(defmeth area grumpy-circle (c)
(format t "How dare you stereotype me!~%")
(funcall (next) c))
順道一提,注意 17.6 實體 (Instances)¶到目前爲止,我們還沒有將類別與實體做區別。我們使用了一個術語來表示兩者,物件(object)。將所有的物件視爲一體是優雅且靈活的,但這非常沒效率。在許多面向物件應用裡,繼承圖的底部會是複雜的。舉例來說,模擬一個交通情況,我們可能有少於十個物件來表示車子的種類,但會有上百個物件來表示特定的車子。由於後者會全部共享少數的優先序列表,創建它們是浪費時間的,並且浪費空間來保存它們。 圖 17.7 定義一個宏 (setf grumpy-circle (inst circle-class))
由於某些物件不再有優先序列表,函數 17.7 新的實現 (New Implementation)¶我們到目前爲止所做的改善都是犧牲靈活性交換而來。在這個系統的開發後期,一個 Lisp 程式通常可以犧牲些許靈活性來獲得好處,這裡也不例外。目前爲止我們使用雜湊表來表示所有的物件。這給我們帶來了超乎我們所需的靈活性,以及超乎我們所想的花費。在這個小節裡,我們會重寫我們的程式,用簡單向量來表示物件。 (defun inst (parent)
(let ((obj (make-hash-table)))
(setf (gethash :parents obj) parent)
obj))
(defun rget (prop obj)
(let ((prec (gethash :preclist obj)))
(if prec
(dolist (c prec)
(multiple-value-bind (val in) (gethash prop c)
(if in (return (values val in)))))
(multiple-value-bind (val in) (gethash prop obj)
(if in
(values val in)
(rget prop (gethash :parents obj)))))))
(defun get-next (obj name)
(let ((prec (gethash :preclist obj)))
(if prec
(some #'(lambda (x) (gethash name x))
(cdr prec))
(get-next (gethash obj :parents) name))))
圖 17.7: 定義實體 這個改變意味著放棄動態定義新屬性的可能性。目前我們可通過引用任何物件,給它定義一個屬性。現在當一個類別被創建時,我們會需要給出一個列表,列出該類有的新屬性,而當實體被創建時,他們會恰好有他們所繼承的屬性。 在先前的實現裡,類別與實體沒有實際區別。一個實體只是一個恰好有一個父類的類別。如果我們改動一個實體的父類,它就變成了一個類別。在新的實現裡,類別與實體有實際區別;它使得將實體轉成類別不再可能。 在圖 17.8-17.10 的程式是一個完整的新實現。圖片 17.8 給創建類別與實體定義了新的運算子。類別與實體用向量來表示。表示類別與實體的向量的前三個元素包含程式自身要用到的資訊,而圖 17.8 的前三個宏是用來引用這些元素的: (defmacro parents (v) `(svref ,v 0))
(defmacro layout (v) `(the simple-vector (svref ,v 1)))
(defmacro preclist (v) `(svref ,v 2))
(defmacro class (&optional parents &rest props)
`(class-fn (list ,@parents) ',props))
(defun class-fn (parents props)
(let* ((all (union (inherit-props parents) props))
(obj (make-array (+ (length all) 3)
:initial-element :nil)))
(setf (parents obj) parents
(layout obj) (coerce all 'simple-vector)
(preclist obj) (precedence obj))
obj))
(defun inherit-props (classes)
(delete-duplicates
(mapcan #'(lambda (c)
(nconc (coerce (layout c) 'list)
(inherit-props (parents c))))
classes)))
(defun precedence (obj)
(labels ((traverse (x)
(cons x
(mapcan #'traverse (parents x)))))
(delete-duplicates (traverse obj))))
(defun inst (parent)
(let ((obj (copy-seq parent)))
(setf (parents obj) parent
(preclist obj) nil)
(fill obj :nil :start 3)
obj))
圖 17.8: 向量實現:創建
因爲這些運算子是宏,他們全都可以被
> (setf *print-array* nil
gemo-class (class nil area)
circle-class (class (geom-class) radius))
#<Simple-Vector T 5 C6205E>
這裡我們創建了兩個類別: > (coerce (layout circle-class) 'list)
(AREA RADIUS)
顯示了五個欄位裡,最後兩個的名稱。 [2]
> (svref circle-class
(+ (position 'area (layout circle-class)) 3))
:NIL
稍後我們會定義存取函數來自動辦到這件事。 最後,函數 > (setf our-circle (inst circle-class))
#<Simple-Vector T 5 C6464E>
比較 (declaim (inline lookup (setf lookup)))
(defun rget (prop obj next?)
(let ((prec (preclist obj)))
(if prec
(dolist (c (if next? (cdr prec) prec) :nil)
(let ((val (lookup prop c)))
(unless (eq val :nil) (return val))))
(let ((val (lookup prop obj)))
(if (eq val :nil)
(rget prop (parents obj) nil)
val)))))
(defun lookup (prop obj)
(let ((off (position prop (layout obj) :test #'eq)))
(if off (svref obj (+ off 3)) :nil)))
(defun (setf lookup) (val prop obj)
(let ((off (position prop (layout obj) :test #'eq)))
(if off
(setf (svref obj (+ off 3)) val)
(error "Can't set ~A of ~A." val obj))))
圖 17.9: 向量實現:存取 現在我們可以創建所需的類別層級及實體,以及需要的函數來讀寫它們的屬性。圖 17.9 的第一個函數是
函數 > (lookup 'area circle-class)
:NIL
由於 (setf (lookup 'area circle-class)
#'(lambda (c)
(* pi (expt (rget 'radius c nil) 2))))
在這個程式裡,和先前的版本一樣,沒有特別區別出方法與槽。一個“方法”只是一個欄位,裡面有著一個函數。這將很快會被一個更方便的前端所隱藏起來。 (declaim (inline run-methods))
(defmacro defprop (name &optional meth?)
`(progn
(defun ,name (obj &rest args)
,(if meth?
`(run-methods obj ',name args)
`(rget ',name obj nil)))
(defun (setf ,name) (val obj)
(setf (lookup ',name obj) val))))
(defun run-methods (obj name args)
(let ((meth (rget name obj nil)))
(if (not (eq meth :nil))
(apply meth obj args)
(error "No ~A method for ~A." name obj))))
(defmacro defmeth (name obj parms &rest body)
(let ((gobj (gensym)))
`(let ((,gobj ,obj))
(defprop ,name t)
(setf (lookup ',name ,gobj)
(labels ((next () (rget ,gobj ',name t)))
#'(lambda ,parms ,@body))))))
圖 17.10: 向量實現:宏介面 圖 17.10 包含了新的實現的最後部分。這段程式碼沒有給程式加入任何威力,但使程式更容易使用。宏 > (defprop radius)
(SETF RADIUS)
> (radius our-circle)
:NIL
> (setf (radius our-circle) 2)
2
如果 最後,函數 現在我們可以達到先前方法定義所有的效果,但更加清晰: (defmeth area circle-class (c)
(* pi (expt (radius c) 2)))
注意我們可以直接呼叫 > (area our-circle)
12.566370614359173
17.8 分析 (Analysis)¶我們現在有了一個適合撰寫實際面向物件程式的嵌入式語言。它很簡單,但就大小來說相當強大。而在典型應用裡,它也會是快速的。在一個典型的應用裡,操作實體應比操作類別更常見。我們重新設計的重點在於如何使得操作實體的花費降低。 在我們的程式裡,創建類別既慢且產生了許多垃圾。如果類別不是在速度爲關鍵考量時創建,這還是可以接受的。會需要速度的是存取函數以及創建實體。這個程式裡的沒有做編譯優化的存取函數大約與我們預期的一樣快。 λ 而創建實體也是如此。且兩個操作都沒有用到構造 (consing)。除了用來表達實體的向量例外。會自然的以爲這應該是動態地配置才對。但我們甚至可以避免動態配置實體,如果我們使用像是 13.4 節所提出的策略。 我們的嵌入式語言是 Lisp 編程的一個典型例子。只不過是一個嵌入式語言就可以是一個例子了。但 Lisp 的特性是它如何從一個小的、受限版本的程式,進化成一個強大但低效的版本,最終演化成快速但稍微受限的版本。 Lisp 惡名昭彰的緩慢不是 Lisp 本身導致(Lisp 編譯器早在 1980 年代就可以產生出與 C 編譯器一樣快的程式碼),而是由於許多程式設計師在第二個階段就放棄的事實。如同 Richard Gabriel 所寫的,
這完全是一個真的論述,但也可以解讀爲讚揚或貶低 Lisp 的論點:
你的程式屬於哪一種解讀完全取決於你。但至少在開發初期,Lisp 使你有犧牲執行速度來換取時間的選擇。 有一件我們範例程式沒有做的很好的事是,它不是一個稱職的 CLOS 模型(除了可能沒有說明難以理解的 我們程式與 CLOS 不同的地方是,方法是屬於某個物件的。這個方法的概念使它們與對第一個參數做派發的函數相同。而當我們使用函數式語法來呼叫方法時,這看起來就跟 Lisp 的函數一樣。相反地,一個 CLOS 的通用函數,可以派發它的任何參數。一個通用函數的組件稱爲方法,而若你將它們定義成只對第一個參數特化,你可以製造出它們是某個類或實體的方法的錯覺。但用物件導向程式設計的訊息傳遞模型來思考 CLOS 最終只會使你困惑,因爲 CLOS 凌駕在物件導向程式設計之上。 CLOS 的缺點之一是它太龐大了,並且 CLOS 費煞苦心的隱藏了物件導向程式設計,其實只不過是改寫 Lisp 的這個事實。本章的例子至少闡明了這一點。如果我們滿足於舊的訊息傳遞模型,我們可以用一頁多一點的程式碼來實現。物件導向程式設計不過是 Lisp 可以做的小事之一而已。更發人深省的問題是,Lisp 除此之外還能做些什麼? 腳註
附錄 A:除錯¶這個附錄示範了如何除錯 Lisp 程式,並給出你可能會遇到的常見錯誤。 中斷迴圈 (Breakloop)¶如果你要求 Lisp 做些它不能做的事,求值過程會被一個錯誤訊息中斷,而你會發現你位於一個稱爲中斷迴圈的地方。中斷迴圈工作的方式取決於不同的實現,但通常它至少會顯示三件事:一個錯誤資訊,一組選項,以及一個特別的提示符。 在中斷迴圈裡,你也可以像在頂層那樣給表達式求值。在中斷迴圈裡,你或許能夠找出錯誤的起因,甚至是修正它,並繼續你程式的求值過程。然而,在一個中斷迴圈裡,你想做的最常見的事是跳出去。多數的錯誤起因於打錯字或是小疏忽,所以通常你只會想終止程式並返回頂層。在下面這個假定的實現裡,我們輸入 > (/ 1 0)
Error: Division by zero.
Options: :abort, :backtrace
>> :abort
>
在這些情況裡,實際上的輸入取決於實現。 當你在中斷迴圈裡,如果一個錯誤發生的話,你會到另一個中斷迴圈。多數的 Lisp 會指出你是在第幾層的中斷迴圈,要嘛通過印出多個提示符,不然就是在提示符前印出數字: >> (/ 2 0)
Error: Division by zero.
Options: :abort, :backtrace, :previous
>>>
現在我們位於兩層深的中斷迴圈。此時我們可以選擇回到前一個中斷迴圈,或是直接返回頂層。 追蹤與回溯 (Traces and Backtraces)¶當你的程式不如你預期的那樣工作時,有時候第一件該解決的事情是,它在做什麼?如果你輸入 一個追蹤通常會根據呼叫樹來縮進。在一個做遍歷的函數,像下面這個函數,它給一個樹的每一個非空元素加上 1, (defun tree1+ (tr)
(cond ((null tr) nil)
((atom tr) (1+ tr))
(t (cons (treel+ (car tr))
(treel+ (cdr tr))))))
一個樹的形狀會因此反映出它被遍歷時的資料結構: > (trace tree1+)
(tree1+)
> (tree1+ '((1 . 3) 5 . 7))
1 Enter TREE1+ ((1 . 3) 5 . 7)
2 Enter TREE1+ (1.3)
3 Enter TREE1+ 1
3 Exit TREE1+ 2
3 Enter TREE1+ 3
3 Exit TREE1+ 4
2 Exit TREE1+ (2 . 4)
2 Enter TREE1+ (5 . 7)
3 Enter TREE1+ 5
3 Exit TREE1+ 6
3 Enter TREE1+ 7
3 Exit TREE1+ 8
2 Exit TREE1+ (6 . 8)
1 Exit TREE1+ ((2 . 4) 6 . 8)
((2 . 4) 6 . 8)
要關掉 一個更靈活的追蹤辦法是在你的程式碼裡插入診斷性的打印語句。如果已經知道結果了,這個經典的方法大概會與複雜的調適工具一樣被使用數十次。這也是爲什麼可以互動地重定義函數式多麼有用的原因。 一個回溯 (backtrace)是一個當前存在棧的呼叫的列表,當一個錯誤中止求值時,會由一個中斷迴圈生成此列表。如果追蹤像是”讓我看看你在做什麼”,一個回溯像是詢問”我們是怎麼到達這裡的?” 在某方面上,追蹤與回溯是互補的。一個追蹤會顯示在一個程式的呼叫樹裡,選定函數的呼叫。一個回溯會顯示在一個程式部分的呼叫樹裡,所有函數的呼叫(路徑爲從頂層呼叫到發生錯誤的地方)。 在一個典型的實現裡,我們可通過在中斷迴圈裡輸入 > (tree1+ ' ( ( 1 . 3) 5 . A))
Error: A is not a valid argument to 1+.
Options: :abort, :backtrace
» :backtrace
(1+ A)
(TREE1+ A)
(TREE1+ (5 . A))
(TREE1+ ((1 . 3) 5 . A))
出現在回溯裡的臭蟲較容易被發現。你可以僅往回檢視呼叫鏈,直到你找到第一個不該發生的事情。另一個函數式編程 (2.12 節)的好處是所有的臭蟲都會在回溯裡出現。在純函數式程式碼裡,每一個可能出錯的呼叫,在錯誤發生時,一定會在棧出現。 一個回溯每個實現所提供的資訊量都不同。某些實現會完整顯示一個所有待呼叫的歷史,並顯示參數。其他實現可能僅顯示呼叫歷史。一般來說,追蹤與回溯解釋型的程式碼會得到較多的資訊,這也是爲什麼你要在確定你的程式可以工作之後,再來編譯。 傳統上我們在解釋器裡除錯程式碼,且只在工作的情況下才編譯。但這個觀點也是可以改變的:至少有兩個 Common Lisp 實現沒有包含解釋器。 當什麼事都沒發生時 (When Noting Happens)¶不是所有的 bug 都會打斷求值過程。另一個常見並可能更危險的情況是,當 Lisp 好像不鳥你一樣。通常這是程式進入無窮迴圈的徵兆。 如果你懷疑你進入了無窮迴圈,解決方法是中止執行,並跳出中斷迴圈。 如果迴圈是用迭代寫成的程式碼,Lisp 會開心地執行到天荒地老。但若是用遞迴寫成的程式碼(沒有做尾遞迴優化),你最終會獲得一個資訊,資訊說 Lisp 把棧的空間給用光了: > (defun blow-stack () (1+ (blow-stack)))
BLOW-STACK
> (blow-stack)
Error: Stack Overflow
在這兩個情況裡,如果你懷疑進入了無窮迴圈,解決辦法是中斷執行,並跳出由於中斷所產生的中斷迴圈。 有時候程式在處理一個非常龐大的問題時,就算沒有進入無窮迴圈,也會把棧的空間用光。雖然這很少見。通常把棧空間用光是編程錯誤的徵兆。 遞迴函數最常見的錯誤是忘記了基本用例 (base case)。用英語來描述遞迴,通常會忽略基本用例。不嚴謹地說,我們可能說“obj 是列表的成員,如果它是列表的第一個元素,或是剩餘列表的成員” 嚴格上來講,應該添加一句“若列表爲空,則 obj 不是列表的成員”。不然我們描述的就是個無窮遞迴了。 在 Common Lisp 裡,如果給入 > (car nil)
NIL
> (cdr nil)
NIL
所以若我們在 (defun our-member (obj lst)
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst))))
要是我們找的物件不在列表裡的話,則會陷入無窮迴圈。當我們到達列表底端而無所獲時,遞迴呼叫會等價於: (our-member obj nil)
在正確的定義中(第十六頁「譯註: 2.7 節」),基本用例在此時會停止遞迴,並返回 如果一個無窮迴圈的起因不是那麼直觀,可能可以通過看看追蹤或回溯來診斷出來。無窮迴圈有兩種。簡單發現的那種是依賴程式結構的那種。一個追蹤或回溯會即刻示範出,我們的 比較難發現的那種,是因爲資料結構有缺陷才發生的無窮迴圈。如果你無意中創建了環狀結構(見 199頁「12.3 節」,遍歷結構的程式碼可能會掉入無窮迴圈裡。這些 bug 很難發現,因爲不在後面不會發生,看起來像沒有錯誤的程式碼一樣。最佳的解決辦法是預防,如同 199 頁所描述的:避免使用破壞性操作,直到程式已經正常工作,且你已準備好要調優程式碼來獲得效率。 如果 Lisp 有不鳥你的傾向,也有可能是等待你完成輸入什麼。在多數系統裡,按下 Enter 是沒有效果的,直到你輸入了一個完整的表達式。這個方法的好事是它允許你輸入多行的表達式。壞事是如果你無意中少了一個閉括號,或是一個閉引號,Lisp 會一直等你,直到你真正完成輸入完整的表達式: > (format t "for example ~A~% 'this)
這裡我們在控制字串的最後忽略了閉引號。在此時按下回車是沒用的,因爲 Lisp 認爲我們還在輸入一個字串。 在某些實現裡,你可以回到上一行,並插入閉引號。在不允許你回到前行的系統,最佳辦法通常是中斷執行,並從中斷迴圈回到頂層。 沒有值或未綁定 (No Value/Unbound)¶一個你最常聽到 Lisp 的抱怨是一個符號沒有值或未綁定。數種不同的問題都用這種方式呈現。 區域變數,如 > (progn
(let ((x 10))
(format t "Here x = ~A. ~%" x))
(format t "But now it's gone...~%")
x)
Here x = 10.
But now it's gone...
Error: X has no value.
我們獲得一個錯誤。當 Lisp 抱怨某些東西沒有值或未綁定時,祂的意思通常是你無意間引用了一個不存在的變數。因爲沒有叫做 一個類似的問題發生在我們無意間將函數引用成變數。舉例來說: > defun foo (x) (+ x 1))
Error: DEFUN has no value
這在第一次發生時可能會感到疑惑: 有可能你真的忘記初始化某個全局變數。如果你沒有給 意料之外的 Nil (Unexpected Nils)¶當函數抱怨傳入 舉例來說,返回一個月有多少天的函數有一個 bug;假設我們忘記十月份了: (defun month-length (mon)
(case mon
((jan mar may jul aug dec) 31)
((apr jun sept nov) 30)
(feb (if (leap-year) 29 28))))
如果有另一個函數,企圖想計算出一個月當中有幾個禮拜, (defun month-weeks (mon) (/ (month-length mon) 7.0))
則會發生下面的情形: > (month-weeks 'oct)
Error: NIL is not a valud argument to /.
問題發生的原因是因爲 在這裡最起碼 bug 與 bug 的臨牀表現是挨著發生的。這樣的 bug 在它們相距很遠時很難找到。要避免這個可能性,某些 Lisp 方言讓跑完 重新命名 (Renaming)¶在某些場合裡(但不是全部場合),有一種特別狡猾的 bug ,起因於重新命名函數或變數,。舉例來說,假設我們定義下列(低效的) 函數來找出雙重巢狀列表的深度: (defun depth (x)
(if (atom x)
1
(1+ (apply #'max (mapcar #'depth x)))))
測試函數時,我們發現它給我們錯誤的答案(應該是 1): > (depth '((a)))
3
起初的 (defun nesting-depth (x)
(if (atom x)
0
(1+ (apply #'max (mapcar #'depth x)))))
當我們再測試上面的例子,它返回同樣的結果: > (nesting-depth '((a)))
3
我們不是修好這個函數了嗎?沒錯,但答案不是來自我們修好的程式碼。我們忘記也改掉遞迴呼叫中的名稱。在遞迴用例裡,我們的新函數仍呼叫先前的 作爲選擇性參數的關鍵字 (Keywords as Optional Parameters)¶若函數同時接受關鍵字與選擇性參數,這通常是個錯誤,無心地提供了關鍵字作爲選擇性參數。舉例來說,函數 (read-from-string string &optional eof-error eof-value
&key start end preserve-whitespace)
這樣一個函數你需要依序提供值,給所有的選擇性參數,再來才是關鍵字參數。如果你忘記了選擇性參數,看看下面這個例子, > (read-from-string "abcd" :start 2)
ABCD
4
則 > (read-from-string "abcd" nil nil :start 2)
CD
4
錯誤宣告 (Misdeclarations)¶第十三章解釋了如何給變數及資料結構做型別宣告。通過給變數做型別宣告,你保證變數只會包含某種型別的值。當產生程式碼時,Lisp 編譯器會依賴這個假定。舉例來說,這個函數的兩個參數都宣告爲 (defun df* (a b)
(declare (double-float a b))
(* a b))
因此編譯器在產生程式碼時,被授權直接將浮點乘法直接硬連接 (hard-wire)到程式碼裡。 如果呼叫 > (df* 2 3)
Error: Interrupt.
如果獲得這樣嚴重的錯誤,通常是由於數值不是先前宣告的型別。 警告 (Warnings)¶有些時候 Lisp 會抱怨一下,但不會中斷求值過程。許多這樣的警告是錯誤的警鐘。一種最常見的可能是由編譯器所產生的,關於未宣告或未使用的變數。舉例來說,在 66 頁「譯註: 6.4 節」, (map-int #'(lambda (x)
(declare (ignore x))
(random 100))
10)
附錄 B:Lisp in Lisp¶這個附錄包含了 58 個最常用的 Common Lisp 運算子。因爲如此多的 Lisp 是(或可以)用 Lisp 所寫成,而由於 Lisp 程式(或可以)相當精簡,這是一種方便解釋語言的方式。 這個練習也證明了,概念上 Common Lisp 不像看起來那樣龐大。許多 Common Lisp 運算子是有用的函式庫;要寫出所有其它的東西,你所需要的運算子相當少。在這個附錄的這些定義只需要:
這裡給出的程式碼作爲一種解釋 Common Lisp 的方式,而不是實現它的方式。在實際的實現上,這些運算子可以更高效,也會做更多的錯誤檢查。爲了方便參找,這些運算子本身按字母順序排列。如果你真的想要這樣定義 Lisp,每個宏的定義需要在任何呼叫它們的程式碼之前。 (defun -abs (n)
(if (typep n 'complex)
(sqrt (+ (expt (realpart n) 2) (expt (imagpart n) 2)))
(if (< n 0) (- n) n)))
(defun -adjoin (obj lst &rest args)
(if (apply #'member obj lst args) lst (cons obj lst)))
(defmacro -and (&rest args)
(cond ((null args) t)
((cdr args) `(if ,(car args) (-and ,@(cdr args))))
(t (car args))))
(defun -append (&optional first &rest rest)
(if (null rest)
first
(nconc (copy-list first) (apply #'-append rest))))
(defun -atom (x) (not (consp x)))
(defun -butlast (lst &optional (n 1))
(nreverse (nthcdr n (reverse lst))))
(defun -cadr (x) (car (cdr x)))
(defmacro -case (arg &rest clauses)
(let ((g (gensym)))
`(let ((,g ,arg))
(cond ,@(mapcar #'(lambda (cl)
(let ((k (car cl)))
`(,(cond ((member k '(t otherwise))
t)
((consp k)
`(member ,g ',k))
(t `(eql ,g ',k)))
(progn ,@(cdr cl)))))
clauses)))))
(defun -cddr (x) (cdr (cdr x)))
(defun -complement (fn)
#'(lambda (&rest args) (not (apply fn args))))
(defmacro -cond (&rest args)
(if (null args)
nil
(let ((clause (car args)))
(if (cdr clause)
`(if ,(car clause)
(progn ,@(cdr clause))
(-cond ,@(cdr args)))
`(or ,(car clause)
(-cond ,@(cdr args)))))))
(defun -consp (x) (typep x 'cons))
(defun -constantly (x) #'(lambda (&rest args) x))
(defun -copy-list (lst)
(labels ((cl (x)
(if (atom x)
x
(cons (car x)
(cl (cdr x))))))
(cons (car lst)
(cl (cdr lst)))))
(defun -copy-tree (tr)
(if (atom tr)
tr
(cons (-copy-tree (car tr))
(-copy-tree (cdr tr)))))
(defmacro -defun (name parms &rest body)
(multiple-value-bind (dec doc bod) (analyze-body body)
`(progn
(setf (fdefinition ',name)
#'(lambda ,parms
,@dec
(block ,(if (atom name) name (second name))
,@bod))
(documentation ',name 'function)
,doc)
',name)))
(defun analyze-body (body &optional dec doc)
(let ((expr (car body)))
(cond ((and (consp expr) (eq (car expr) 'declare))
(analyze-body (cdr body) (cons expr dec) doc))
((and (stringp expr) (not doc) (cdr body))
(if dec
(values dec expr (cdr body))
(analyze-body (cdr body) dec expr)))
(t (values dec doc body)))))
這個定義不完全正確,參見 (defmacro -do (binds (test &rest result) &rest body)
(let ((fn (gensym)))
`(block nil
(labels ((,fn ,(mapcar #'car binds)
(cond (,test ,@result)
(t (tagbody ,@body)
(,fn ,@(mapcar #'third binds))))))
(,fn ,@(mapcar #'second binds))))))
(defmacro -dolist ((var lst &optional result) &rest body)
(let ((g (gensym)))
`(do ((,g ,lst (cdr ,g)))
((atom ,g) (let ((,var nil)) ,result))
(let ((,var (car ,g)))
,@body))))
(defun -eql (x y)
(typecase x
(character (and (typep y 'character) (char= x y)))
(number (and (eq (type-of x) (type-of y))
(= x y)))
(t (eq x y))))
(defun -evenp (x)
(typecase x
(integer (= 0 (mod x 2)))
(t (error "non-integer argument"))))
(defun -funcall (fn &rest args) (apply fn args))
(defun -identity (x) x)
這個定義不完全正確:表達式 (defmacro -let (parms &rest body)
`((lambda ,(mapcar #'(lambda (x)
(if (atom x) x (car x)))
parms)
,@body)
,@(mapcar #'(lambda (x)
(if (atom x) nil (cadr x)))
parms)))
(defun -list (&rest elts) (copy-list elts))
(defun -listp (x) (or (consp x) (null x)))
(defun -mapcan (fn &rest lsts)
(apply #'nconc (apply #'mapcar fn lsts)))
(defun -mapcar (fn &rest lsts)
(cond ((member nil lsts) nil)
((null (cdr lsts))
(let ((lst (car lsts)))
(cons (funcall fn (car lst))
(-mapcar fn (cdr lst)))))
(t
(cons (apply fn (-mapcar #'car lsts))
(apply #'-mapcar fn
(-mapcar #'cdr lsts))))))
(defun -member (x lst &key test test-not key)
(let ((fn (or test
(if test-not
(complement test-not))
#'eql)))
(member-if #'(lambda (y)
(funcall fn x y))
lst
:key key)))
(defun -member-if (fn lst &key (key #'identity))
(cond ((atom lst) nil)
((funcall fn (funcall key (car lst))) lst)
(t (-member-if fn (cdr lst) :key key))))
(defun -mod (n m)
(nth-value 1 (floor n m)))
(defun -nconc (&optional lst &rest rest)
(if rest
(let ((rest-conc (apply #'-nconc rest)))
(if (consp lst)
(progn (setf (cdr (last lst)) rest-conc)
lst)
rest-conc))
lst))
(defun -not (x) (eq x nil))
(defun -nreverse (seq)
(labels ((nrl (lst)
(let ((prev nil))
(do ()
((null lst) prev)
(psetf (cdr lst) prev
prev lst
lst (cdr lst)))))
(nrv (vec)
(let* ((len (length vec))
(ilimit (truncate (/ len 2))))
(do ((i 0 (1+ i))
(j (1- len) (1- j)))
((>= i ilimit) vec)
(rotatef (aref vec i) (aref vec j))))))
(if (typep seq 'vector)
(nrv seq)
(nrl seq))))
(defun -null (x) (eq x nil))
(defmacro -or (&optional first &rest rest)
(if (null rest)
first
(let ((g (gensym)))
`(let ((,g ,first))
(if ,g
,g
(-or ,@rest))))))
這兩個 Common Lisp 沒有,但這裡有幾的定義會需要用到。 (defun pair (lst)
(if (null lst)
nil
(cons (cons (car lst) (cadr lst))
(pair (cddr lst)))))
(defun -pairlis (keys vals &optional alist)
(unless (= (length keys) (length vals))
(error "mismatched lengths"))
(nconc (mapcar #'cons keys vals) alist))
(defmacro -pop (place)
(multiple-value-bind (vars forms var set access)
(get-setf-expansion place)
(let ((g (gensym)))
`(let* (,@(mapcar #'list vars forms)
(,g ,access)
(,(car var) (cdr ,g)))
(prog1 (car ,g)
,set)))))
(defmacro -prog1 (arg1 &rest args)
(let ((g (gensym)))
`(let ((,g ,arg1))
,@args
,g)))
(defmacro -prog2 (arg1 arg2 &rest args)
(let ((g (gensym)))
`(let ((,g (progn ,arg1 ,arg2)))
,@args
,g)))
(defmacro -progn (&rest args) `(let nil ,@args))
(defmacro -psetf (&rest args)
(unless (evenp (length args))
(error "odd number of arguments"))
(let* ((pairs (pair args))
(syms (mapcar #'(lambda (x) (gensym))
pairs)))
`(let ,(mapcar #'list
syms
(mapcar #'cdr pairs))
(setf ,@(mapcan #'list
(mapcar #'car pairs)
syms)))))
(defmacro -push (obj place)
(multiple-value-bind (vars forms var set access)
(get-setf-expansion place)
(let ((g (gensym)))
`(let* ((,g ,obj)
,@(mapcar #'list vars forms)
(,(car var) (cons ,g ,access)))
,set))))
(defun -rem (n m)
(nth-value 1 (truncate n m)))
(defmacro -rotatef (&rest args)
`(psetf ,@(mapcan #'list
args
(append (cdr args)
(list (car args))))))
(defun -second (x) (cadr x))
(defmacro -setf (&rest args)
(if (null args)
nil
`(setf2 ,@args)))
(defmacro setf2 (place val &rest args)
(multiple-value-bind (vars forms var set)
(get-setf-expansion place)
`(progn
(let* (,@(mapcar #'list vars forms)
(,(car var) ,val))
,set)
,@(if args `((setf2 ,@args)) nil))))
(defun -signum (n)
(if (zerop n) 0 (/ n (abs n))))
(defun -stringp (x) (typep x 'string))
(defun -tailp (x y)
(or (eql x y)
(and (consp y) (-tailp x (cdr y)))))
(defun -third (x) (car (cdr (cdr x))))
(defun -truncate (n &optional (d 1))
(if (> n 0) (floor n d) (ceiling n d)))
(defmacro -typecase (arg &rest clauses)
(let ((g (gensym)))
`(let ((,g ,arg))
(cond ,@(mapcar #'(lambda (cl)
`((typep ,g ',(car cl))
(progn ,@(cdr cl))))
clauses)))))
(defmacro -unless (arg &rest body)
`(if (not ,arg)
(progn ,@body)))
(defmacro -when (arg &rest body)
`(if ,arg (progn ,@body)))
(defun -1+ (x) (+ x 1))
(defun -1- (x) (- x 1))
(defun ->= (first &rest rest)
(or (null rest)
(and (or (> first (car rest)) (= first (car rest)))
(apply #'->= rest))))
附錄 C:Common Lisp 的改變¶目前的 ANSI Common Lisp 與 1984 年由 Guy Steele 一書 Common Lisp: the Language 所定義的 Common Lisp 有著本質上的不同。同時也與 1990 年該書的第二版大不相同,雖然差別比較小。本附錄總結了重大的改變。1990年之後的改變獨自列在最後一節。 附錄 D:語言參考手冊¶備註¶本節既是備註亦作爲參考文獻。所有列於此的書籍與論文皆值得閱讀。 譯註: 備註後面跟隨的數字即書中的頁碼 備註 viii (Notes viii)¶Steele, Guy L., Jr., Scott E. Fahlman, Richard P. Gabriel, David A. Moon, Daniel L. Weinreb , Daniel G. Bobrow, Linda G. DeMichiel, Sonya E. Keene, Gregor Kiczales, Crispin Perdue, Kent M. Pitman, Richard C. Waters, 以及 John L White。 Common Lisp: the Language, 2nd Edition. Digital Press, Bedford (MA), 1990. 備註 1 (Notes 1)¶McCarthy, John. Recursive Functions of Symbolic Expressions and their Computation by Machine, Part I. CACM, 3:4 (April 1960), pp. 184-195. McCarthy, John. History of Lisp. In Wexelblat, Richard L. (Ed.) Histroy of Programming Languages. Academic Press, New York, 1981, pp. 173-197. 備註 3 (Notes 3)¶Brooks, Frederick P. The Mythical Man-Month. Addison-Wesley, Reading (MA), 1975, p. 16. Rapid prototyping is not just a way to write programs faster or better. It is a way to write programs that otherwise might not get written at all. Even the most ambitious people shrink from big undertakings. It’s easier to start something if one can convince oneself (however speciously) that it won’t be too much work. That’s why so many big things have begun as small things. Rapid prototyping lets us start small. 備註 4 (Notes 4)¶同上, 第 i 頁。 備註 5 (Notes 5)¶Murray, Peter and Linda. The Art of the Renaissance. Thames and Hudson, London, 1963, p.85. 備註 5-2 (Notes 5-2)¶Janson, W.J. History of Art, 3rd Edition. Abrams, New York, 1986, p. 374. The analogy applies, of course, only to paintings done on panels and later on canvases. Well-paintings continued to be done in fresco. Nor do I mean to suggest that painting styles were driven by technological change; the opposite seems more nearly true. 備註 12 (Notes 12)¶
備註 17 (Notes 17)¶對遞迴概念有困擾的讀者,可以查閱下列的書籍: Touretzky, David S. Common Lisp: A Gentle Introduction to Symbolic Computation. Benjamin/Cummings, Redwood City (CA), 1990, Chapter 8. Friedman, Daniel P., and Matthias Felleisen. The Little Lisper. MIT Press, Cambridge, 1987. 譯註:這本書有再版,可在這裡找到。 備註 26 (Notes 26)¶In ANSI Common Lisp there is also a 備註 28 (Notes 28)¶Gabriel, Richard P. Lisp Good News, Bad News, How to Win Big AI Expert, June 1991, p.34. 備註 46 (Notes 46)¶Another thing to be aware of when using sort: it does not guarantee to preserve the order of elements judged equal by the comparison function. For example, if you sort 備註 61 (Notes 61)¶A lot has been said about the benefits of comments, and little or nothing about their cost. But they do have a cost. Good code, like good prose, comes from constant rewriting. To evolve, code must be malleable and compact. Interlinear comments make programs stiff and diffuse, and so inhibit the evolution of what they describe. 備註 62 (Notes 62)¶Though most implementations use the ASCII character set, the only ordering that Common Lisp guarantees for characters is as follows: the 26 lowercase letters are in alphabetically ascending order, as are the uppercase letters, and the digits from 0 to 9. 備註 76 (Notes 76)¶The standard way to implement a priority queue is to use a structure called a heap. See: Sedgewick, Robert. Algorithms. Addison-Wesley, Reading (MA), 1988. 備註 81 (Notes 81)¶The definition of progn sounds a lot like the evaluation rule for Common Lisp function calls (page 9). Though (defun our-progn (ftrest args)
(car (last args)))
This would be horribly inefficient, but functionally equivalent to the real 備註 84 (Notes 84)¶The analogy to a lambda expression breaks down if the variable names are symbols that have special meanings in a parameter list. For example, (let ((&key 1) (&optional 2)))
is correct, but the corresponding lambda expression ((lambda (ftkey ftoptional)) 1 2)
is not. The same problem arises if you try to define do in terms of 備註 89 (Notes 89)¶Steele, Guy L., Jr., and Richard P. Gabriel. The Evolution of Lisp. ACM SIGPLANNotices 28:3 (March 1993). The example in the quoted passage was translated from Scheme into Common Lisp. 備註 91 (Notes 91)¶To make the time look the way people expect, you would want to ensure that minutes and seconds are represented with two digits, as in: (defun get-time-string ()
(multiple-value-bind (s m h) (get-decoded-time)
(format nil "~A:~2,,,'0@A:~2,,,'O@A" h m s)))
備註 94 (Notes 94)¶In a letter of March 18 (old style) 1751, Chesterfield writes: “It was notorious, that the Julian Calendar was erroneous, and had overcharged the solar year with eleven days. Pope Gregory the Thirteenth corrected this error [in 1582]; his reformed calendar was immediately received by all the Catholic powers of Europe, and afterwards adopted by all the Protestant ones, except Russia, Sweden, and England. It was not, in my opinion, very honourable for England to remain in a gross and avowed error, especially in such company; the inconveniency of it was likewise felt by all those who had foreign correspondences, whether political or mercantile. I determined, therefore, to attempt the reformation; I consulted the best lawyers, and the most skillful astronomers, and we cooked up a bill for that purpose. But then my difficulty began; I was to bring in this bill, which was necessarily composed of law jargon and astronomical calculations, to both of which I am an utter stranger. However, it was absolutely necessary to make the House of Lords think that I knew something of the matter; and also to make them believe that they knew something of it themselves, which they do not. For my own part, I could just as soon have talked Celtic or Sclavonian to them, as astronomy, and they would have understood me full as well; so I resolved to do better than speak to the purpose, and to please instead of informing them. I gave them, therefore, only an historical account of calendars, from the Egyptian down to the Gregorian, amusing them now and then with little episodes; but I was particularly attentive to the choice of my words, to the harmony and roundness of my periods, to my elocution, to my action. This succeeded, and ever will succeed; they thought I informed them, because I pleased them; and many of them said I had made the whole very clear to them; when, God knows, I had not even attempted it.” See: Roberts, David (Ed.) Lord Chesterfield’s Letters. Oxford University Press, Oxford, 1992. 備註 95 (Notes 95)¶In Common Lisp, a universal time is an integer representing the number of seconds since the beginning of 1900. The functions (defun num->date (n)
(multiple-value-bind (ig no re d m y)
(decode-universal-time n)
(values d m y)))
(defun date->num (d m y)
(encode-universal-time 1 0 0 d m y))
(defun date+ (d m y n)
(num->date (+ (date->num d m y)
(* 60 60 24 n))))
Besides the range limit, this approach has the disadvantage that dates tend not to be fixnums. 備註 100 (Notes 100)¶Although a call to (defstruct marble
color)
The following function takes a list of marbles and returns their color, if they all have the same color, or n i l if they have different colors: (defun uniform-color (1st)
(let ((c (marble-color (car 1st))))
(dolist (m (cdr 1st))
(unless (eql (marble-color m) c)
(return nil)))
c))
Although (defun (setf uniform-color) (val 1st)
(dolist (m 1st)
(setf (marble-color m) val)))
we can say (setf (uniform-color *marbles*) 'red)
to make the color of each element of 備註 100-2 (Notes 100-2)¶In older Common Lisp implementations, you have to use (defun (setf primo) (val 1st) (setf (car 1st) val))
is equivalent to (defsetf primo set-primo)
(defun set-primo (1st val) (setf (car 1st) val))
備註 106 (Notes 106)¶C, for example, lets you pass a pointer to a function, but there’s less you can pass in a function (because C doesn’t have closures) and less the recipient can do with it (because C has no equivalent of apply). What’s more, you are in principle supposed to declare the type of the return value of the function you pass a pointer to. How, then, could you write 備註 109 (Notes 109)¶For many examples of the versatility of closures, see: Abelson, Harold, and Gerald Jay Sussman, with Julie Sussman. Structure and Interpretation of Computer Programs. MIT Press, Cambridge, 1985. 備註 109-2 (Notes 109-2)¶For more information about Dylan, see: Shalit, Andrew, with Kim Barrett, David Moon, Orca Starbuck, and Steve Strassmann. Dylan Interim Reference Manual. Apple Computer, 1994. At the time of printing this document was accessible from several sites, including http://www.harlequin.com and http://www.apple.com. Scheme is a very small, clean dialect of Lisp. It was invented by Guy L. Steele Jr. and Gerald J. Sussman in 1975, and is currently defined by: Clinger, William, and Jonathan A. Rees (Eds.) \(Revised^4\) Report on the Algorithmic Language Scheme. 1991. This report, and various implementations of Scheme, were at the time of printing available by anonymous FTP from swiss-ftp.ai.mit.edu:pub. There are two especially good textbooks that use Scheme—Structure and Interpretation (see preceding note) and: Springer, George and Daniel P. Friedman. Scheme and the Art of Programming. MIT Press, Cambridge, 1989. 備註 112 (Notes 112)¶The most horrible Lisp bugs may be those involving dynamic scope. Such errors almost never occur in Common Lisp, which has lexical scope by default. But since so many of the Lisps used as extension languages still have dynamic scope, practicing Lisp programmers should be aware of its perils. One bug that can arise with dynamic scope is similar in spirit to variable capture (page 166). You pass one function as an argument to another. The function passed as an argument refers to some variable. But within the function that calls it, the variable has a new and unexpected value. Suppose, for example, that we wrote a restricted version of mapcar as follows: (defun our-mapcar (fn x)
(if (null x)
nil (cons (funcall fn (car x))
(our-mapcar fn (cdr x)))))
Then suppose that we used this function in another function, (defun add-to-all (1st x)
(our-mapcar #'(lambda (num) (+ num x))
1st))
In Common Lisp this code works fine, but in a Lisp with dynamic scope it would generate an error. The function passed as an argument to 備註 123 (Notes 123)¶Newer implementations of Common Lisp include avariable 備註 125 (Notes 125)¶There are a number of ingenious algorithms for fast string-matching, but string-matching in text files is one of the cases where the brute-force approach is still reasonably fast. For more on string-matching algorithms, see: Sedgewick, Robert. Algorithms. Addison-Wesley, Reading (MA), 1988. 備註 141 (Notes 141)¶In 1984 CommonLisp, reduce did not take a (defun random-next (prev)
(let* ((choices (gethash prev *words*))
(i (random (let ((x 0))
(dolist (c choices)
(incf x (cdr c)))
x))))
(dolist (pair choices)
(if (minusp (decf i (cdr pair)))
(return (car pair))))))
備註 141-2 (Notes 141-2)¶In 1989, a program like Henley was used to simulate netnews postings by well-known flamers. The fake postings fooled a significant number of readers. Like all good hoaxes, this one had an underlying point. What did it say about the content of the original flames, or the attention with which they were read, that randomly generated postings could be mistaken for the real thing? One of the most valuable contributions of artificial intelligence research has been to teach us which tasks are really difficult. Some tasks turn out to be trivial, and some almost impossible. If artificial intelligence is concerned with the latter, the study of the former might be called artificial stupidity. A silly name, perhaps, but this field has real promise—it promises to yield programs that play a role like that of control experiments. Speaking with the appearance of meaning is one of the tasks that turn out to be surprisingly easy. People’s predisposition to find meaning is so strong that they tend to overshoot the mark. So if a speaker takes care to give his sentences a certain kind of superficial coherence, and his audience are sufficiently credulous, they will make sense of what he says. This fact is probably as old as human history. But now we can give examples of genuinely random text for comparison. And if our randomly generated productions are difficult to distinguish from the real thing, might that not set people to thinking? The program shown in Chapter 8 is about as simple as such a program could be, and that is already enough to generate “poetry” that many people (try it on your friends) will believe was written by a human being. With programs that work on the same principle as this one, but which model text as more than a simple stream of words, it will be possible to generate random text that has even more of the trappings of meaning. For a discussion of randomly generated poetry as a legitimate literary form, see: Low, Jackson M. Poetry, Chance, Silence, Etc. In Hall, Donald (Ed.) Claims for Poetry. University of Michigan Press, Ann Arbor, 1982. You bet. Thanks to the Online Book Initiative, ASCII versions of many classics are available online. At the time of printing, they could be obtained by anonymous FTP from ftp.std.com:obi. See also the Emacs Dissociated Press feature, which uses an equivalent algorithm to scramble a buffer. 備註 150 (Notes 150)¶下面這個函數會顯示在一個給定實現中,16 個用來標示浮點表示法的限制的全局常數: (defun float-limits ()
(dolist (m '(most least))
(dolist (s '(positive negative))
(dolist (f '(short single double long))
(let ((n (intern (string-upcase
(format nil "~A-~A-~A-float"
m s f)))))
(format t "~30A ~A ~%" n (symbol-value n)))))))
備註 164 (Notes 164)¶快速排序演算法由霍爾於 1962 年發表,並被描述在 Knuth, D. E. Sorting and Searching. Addison-Wesley, Reading (MA), 1973.一書中。 備註 173 (Notes 173)¶Foderaro, John K. Introduction to the Special Lisp Section. CACM 34:9 (Setember 1991), p.27 備註 176 (Notes 176)¶關於 CLOS 更詳細的資訊,參考下列書目: Keene, Sonya E. Object Oriented Programming in Common Lisp , Addison-Wesley, Reading (MA), 1989 Kiczales, Gregor, Jim des Rivieres, and Daniel G. Bobrow. The Art of the Metaobject Protocol MIT Press, Cambridge, 1991 備註 178 (Notes 178)¶讓我們再回放剛剛的句子一次:我們甚至不需要看程式中其他的程式碼一眼,就可以完成種種的改動。這個想法或許對某些讀者聽起來擔憂地熟悉。這是寫出麵條式程式碼的食譜。 物件導向模型使得通過一點一點的來構造程式變得簡單。但這通常意味著,在實踐上它提供了一種有結構的方法來寫出麵條式程式碼。這不一定是壞事,但也不會是好事。 很多現實世界中的程式碼是麵條式程式碼,這也許不能很快改變。針對那些終將成爲麵條式程式碼的程式來說,物件導向模型是好的:它們最起碼會是有結構的麵條。但針對那些也許可以避免誤入崎途的程式來說,面向物件抽象只是更加危險的,而不是有用的。 備註 183 (Notes 183)¶When an instance would inherit a slot with the same name from several of its superclasses, the instance inherits a single slot that combines the properties of the slots in the superclasses. The way combination is done varies from property to property:
備註 191 (Notes 191)¶You can avoid explicitly uninterning the names of slots that you want to be encapsulated by using uninterned symbols as the names to start with: (progn
(defclass counter () ((#1=#:state :initform 0)))
(defmethod increment ((c counter))
(incf (slot-value c '#1#)))
(defmethod clear ((c counter))
(setf (slot-value c '#1#) 0)))
The (defvar *symtab* (make-hash-table :test #'equal))
(defun pseudo-intern (name)
(or (gethash name *symtab*)
(setf (gethash name *symtab*) (gensym))))
(set-dispatch-macro-character #\# #\[
#'(lambda (stream char1 char2)
(do ((acc nil (cons char acc))
(char (read-char stream) (read-char stream)))
((eql char #\]) (pseudo-intern acc)))))
Then it would be possible to say just: (defclass counter () ((#[state] :initform 0)))
(defmethod increment ((c counter))
(incf (slot-value c '#[state])))
(defmethod clear ((c counter))
(setf (slot-value c '#[state]) 0))
備註 204 (Notes 204)¶下面這個宏將新元素推入二元搜索樹: (defmacro bst-push (obj bst <)
(multiple-value-bind (vars forms var set access)
(get-setf-expansion bst)
(let ((g (gensym)))
`(let* ((,g ,obj)
,@(mapcar #'list vars forms)
(,(car var) (bst-insert! ,g ,access ,<)))
,set))))
備註 213 (Notes 213)¶Knuth, Donald E. Structured Programming with goto Statements. Computing Surveys , 6:4 (December 1974), pp. 261-301 備註 214 (Notes 214)¶Knuth, Donald E. Computer Programming as an Art In ACM Turing Award Lectures: The First Twenty Years. ACM Press, 1987 This paper and the preceding one are reprinted in: Knuth, Donald E. Literate Programming. CSLI Lecture Notes #27, Stanford University Center for the Study of Language and Information, Palo Alto, 1992. 備註 216 (Notes 216)¶Steele, Guy L., Jr. Debunking the “Expensive Procedure Call” Myth or, Procedural Call Implementations Considered Harmful or, LAMBDA: The Ultimate GOTO. Proceedings of the National Conference of the ACM, 1977, p. 157. Tail-recursion optimization should mean that the compiler will generate the same code for a tail-recursive function as it would for the equivalent 備註 217 (Notes 217)¶For some examples of calls to disassemble on various processors, see: Norvig, Peter. Paradigms ofArtificial Intelligence Programming: Case Studies in Common Lisp. Morgan Kaufmann, San Mateo (CA), 1992. 備註 218 (Notes 218)¶A lot of the increased popularity of object-oriented programming is more specifically the increased popularity of C++, and this in turn has a lot to do with typing. C++ gives you something that seems like a miracle in the conceptual world of C: the ability to define operators that work for different types of arguments. But you don’t need an object-oriented language to do this—all you need is run-time typing. And indeed, if you look at the way people use C++, the class hierarchies tend to be flat. C++ has become so popular not because people need to write programs in terms of classes and methods, but because people need a way around the restrictions imposed by C’s approach to typing. 備註 219 (Notes 219)¶Macros can make declarations easier. The following macro expects a type name and an expression (probably numeric), and expands the expression so that all arguments, and all intermediate results, are declared to be of that type. If you wanted to ensure that an expression e was evaluated using only fixnum arithmetic, you could say (defmacro with-type (type expr)
`(the ,type ,(if (atom expr)
expr
(expand-call type (binarize expr)))))
(defun expand-call (type expr)
`(,(car expr) ,@(mapcar #'(lambda (a)
`(with-type ,type ,a))
(cdr expr))))
(defun binarize (expr)
(if (and (nthcdr 3 expr)
(member (car expr) '(+ - * /)))
(destructuring-bind (op a1 a2 . rest) expr
(binarize `(,op (,op ,a1 ,a2) ,@rest)))
expr))
The call to binarize ensures that no arithmetic operator is called with more than two arguments. As the Lucid reference manual points out, a call like (the fixnum (+ (the fixnum a)
(the fixnum b)
(the fixnum c)))
still cannot be compiled into fixnum additions, because the intermediate results (e.g. a + b) might not be fixnums. Using (defun poly (a b x)
(with-type fixnum (+ (* a (expt x 2)) (* b x))))
If you wanted to do a lot of fixnum arithmetic, you might even want to define a read-macro that would expand into a 備註 224 (Notes 224)¶在許多 Unix 系統裡, 備註 226 (Notes 229)¶T is a dialect of Scheme with many useful additions, including support for pools. For more on T, see: Rees, Jonathan A., Norman I. Adams, and James R. Meehan. The T Manual, 5th Edition. Yale University Computer Science Department, New Haven, 1988. The T manual, and T itself, were at the time of printing available by anonymous FTP from hing.lcs.mit.edu:pub/t3.1 . 備註 229 (Notes 229)¶The difference between specifications and programs is a difference in degree, not a difference in kind. Once we realize this, it seems strange to require that one write specifications for a program before beginning to implement it. If the program has to be written in a low-level language, then it would be reasonable to require that it be described in high-level terms first. But as the programming language becomes more abstract, the need for specifications begins to evaporate. Or rather, the implementation and the specifications can become the same thing. If the high-level program is going to be re-implemented in a lower-level language, it starts to look even more like specifications. What Section 13.7 is saying, in other words, is that the specifications for C programs could be written in Lisp. 備註 230 (Notes 230)¶Benvenuto Cellini’s story of the casting of his Perseus is probably the most famous (and the funniest) account of traditional bronze-casting: Cellini, Benvenuto. Autobiography. Translated by George Bull, Penguin Books, Harmondsworth, 1956. 備註 239 (Notes 239)¶Even experienced Lisp hackers find packages confusing. Is it because packages are gross, or because we are not used to thinking about what happens at read-time? There is a similar kind of uncertainty about def macro, and there it does seem that the difficulty is in the mind of the beholder. A good deal of work has gone into finding a more abstract alternative to def macro. But def macro is only gross if you approach it with the preconception (common enough) that defining a macro is like defining a function. Then it seems shocking that you suddenly have to worry about variable capture. When you think of macros as what they are, transformations on source code, then dealing with variable capture is no more of a problem than dealing with division by zero at run-time. So perhaps packages will turn out to be a reasonable way of providing modularity. It is prima facie evidence on their side that they resemble the techniques that programmers naturally use in the absence of a formal module system. 備註 242 (Notes 242)¶It might be argued that 備註 248 (Notes 248)¶關於更深入講述邏輯推論的資料,參見:Stuart Russell 及 Peter Norvig 所著的 Artificial Intelligence: A Modern Approach。 備註 273 (Notes 273)¶Because the program in Chapter 17 takes advantage of the possibility of having a (proclaim '(inline lookup set-lookup))
(defsetf lookup set-lookup)
(defun set-lookup (prop obj val)
(let ((off (position prop (layout obj) :test #'eq)))
(if off
(setf (svref obj (+ off 3)) val)
(error "Can't set ~A of ~A." val obj))))
(defmacro defprop (name &optioanl meth?)
`(progn
(defun ,name (obj &rest args)
,(if meth?
`(run-methods obj ',name args)
`(rget ',name obj nil)))
(defsetf ,name (obj) (val)
`(setf (lookip ',',name ,obj) ,val))))
備註 276 (Notes 276)¶If (defmacro defmeth (name obj parms &rest body)
(let ((gobj (gensym)))
`(let ((,gobj ,obj))
(setf (gethash ',name ,gobj)
#'(lambda ,parms
(labels ((next ()
(funcall (get-next ,gobj ',name)
,@parms)))
,@body))))))
then it would be possible to invoke the next method simply by calling (defmeth area grumpy-circle (c)
(format t "How dare you stereotype me!""/,")
(next))
備註 284 (Notes 284)¶For really fast access to slots we would use the following macro: (defmacro with-slotref ((name prop class) &rest body)
(let ((g (gensym)))
`(let ((,g (+ 3 (position ,prop (layout ,class)
:test #'eq))))
(macrolet ((,name (obj) `(svref ,obj ,',g)))
,@body))))
It defines a local macro that refers directly to the vector element corresponding to a slot. If in some segment of code you wanted to refer to the same slot in many instances of the same class, with this macro the slot references would be straight For example, if the balloon class is defined as follows, (setf balloon-class (class nil size))
then this function pops (in the old sense) a list of ballons: (defun popem (ballons)
(with-slotref (bsize 'size balloon-class)
(dolist (b ballons)
(setf (bsize b) 0))))
備註 284-2 (Notes 284-2)¶Gabriel, Richard P. Lisp Good News, Bad News, How to Win Big AI Expert, June 1991, p.35. 早在 1973 年, Richard Fateman 已經能證明在 PDP-10 主機上, MacLisp 編譯器比製造商的 FORTRAN 編譯器,產生出更快速的程式碼。 備註 399 (Notes 399)¶It’s easiest to understand backquote if we suppose that backquote and comma are like quote, and that (defun eval2 (expr)
(case (and (consp expr) (car expr))
(comma (error "unmatched comma"))
(bq (eval-bq (second expr) 1))
(t (eval expr))))
(defun eval-bq (expr n)
(cond ((atom expr)
expr)
((eql (car expr) 'comma)
(if (= n 1)
(eval2 (second expr))
(list 'comma (eval-bq (second expr)
(1- n)))))
((eql (car expr) 'bq)
(list 'bq (eval-bq (second expr) (1+ n))))
(t
(cons (eval-bq (car expr) n)
(eval-bq (cdr expr) n)))))
In > (setf x 'a a 1 y 'b b 2)
2
> (eval2 '(bq (bq (w (comma x) (comma (comma y))))))
(BQ (W (COMMA X) (COMMA B)))
> (eval2 *)
(W A 2)
At some point a particularly remarkable molecule was formed by accident. We will call it the Replicator. It may not necessarily have been the biggest or the most complex molecule around, but it had the extraordinary property of being able to create copies of itself. Richard Dawkins The Selfish Gene We shall first define a class of symbolic expressions in terms of ordered pairs and lists. Then we shall define five elementary functions and predicates, and build from them by composition, conditional expressions, and recursive definitions an extensive class of functions of which we shall give a number of examples. We shall then show how these functions themselves can be expressed as symbolic expressions, and we shall define a universal function apply that allows us to compute from the expression for a given function its value for given arguments. John McCarthy Recursive Functions of Symbolic Expressions and their Computation by Machine, Part I |
简体中文¶前言¶本书的目的是快速及全面的教你 Common Lisp 的有关知识。它实际上包含两本书。前半部分用大量的例子来解释 Common Lisp 里面重要的概念。后半部分是一个最新 Common Lisp 辞典,涵盖了所有 ANSI Common Lisp 的操作符。 这本书面向的读者¶ANSI Common Lisp 这本书适合学生或者是专业的程序员去读。本书假设读者阅读前没有 Lisp 的相关知识。有别的程序语言的编程经验也许对读本书有帮助,但也不是必须的。本书从解释 Lisp 中最基本的概念开始,并对于 Lisp 最容易迷惑初学者的地方进行特别的强调。 本书也可以作为教授 Lisp 编程的课本,也可以作为人工智能课程和其他编程语言课程中,有关 Lisp 部分的参考书。想要学习 Lisp 的专业程序员肯定会很喜欢本书所采用的直截了当、注重实践的方法。那些已经在使用 Lisp 编程的人士将会在本书中发现许多有用的实例,此外,本书也是一本方便的 ANSI Common Lisp 参考书。 如何使用这本书¶学习 Lisp 最好的办法就是拿它来编程。况且在学习的同时用你学到的技术进行编程,也是非常有趣的一件事。编写本书的目的就是让读者尽快的入门,在对 Lisp 进行简短的介绍之后, 第 2 章开始用 21 页的内容,介绍了着手编写 Lisp 程序时可能会用到的所有知识。 3-9 章讲解了 Lisp 里面一些重要的知识点。这些章节特别强调了一些重要的概念,比如指针在 Lisp 中扮演的角色,如何使用递归来解决问题,以及第一级函数的重要性等等。 针对那些想要更深入了解 Lisp 的读者: 10-14 章包含了宏、CLOS、列表操作、程序优化,以及一些更高级的课题,比如包和读取宏。 15-17 章通过 3 个 Common Lisp 的实际应用,总结了之前章节所讲解的知识:一个是进行逻辑推理的程序,另一个是 HTML 生成器,最后一个是针对面向对象编程的嵌入式语言。 本书的最后一部分包含了 4 个附录,这些附录应该对所有的读者都有用: 附录 A-D 包括了一个如何调试程序的指南, 58 个 Common Lisp 操作符的源程序,一个关于 ANSI Common Lisp 和以前的 Lisp 语言区别的总结,以及一个包括所有 ANSI Common Lisp 的参考手册。 本书还包括一节备注。这些备注包括一些说明,一些参考条目,一些额外的代码,以及一些对偶然出现的不正确表述的纠正。备注在文中用一个小圆圈来表示,像这样:○ Note 译注: 由于小圈圈 ○ 实在太不明显了,译文中使用 λ 符号来表示备注。 代码¶虽然本书介绍的是 ANSI Common Lisp ,但是本书中的代码可以在任何版本的 Common Lisp 中运行。那些依赖 Lisp 语言新特性的例子的旁边,会有注释告诉你如何把它们运行于旧版本的 Lisp 中。 本书中所有的代码都可以在互联网上下载到。你可以在网络上找到这些代码,它们还附带着一个免费软件的链接,一些过去的论文,以及 Lisp 的 FAQ 。还有很多有关 Lisp 的资源可以在此找到: http://www.eecs.harvard.edu/onlisp/ 源代码可以在此 FTP 服务器上下载: ftp://ftp.eecs.harvard.edu:/pub/onlisp/ 读者的问题和意见可以发送到 pg@eecs.harvard.edu 。 Tip 译注:下载的链接都坏掉了,本书的代码可以到此下载:https://raw.github.com/acl-translation/acl-chinese/master/code/acl2.lisp On Lisp¶在整本 On Lisp 书中,我一直试着指出一些 Lisp 独一无二的特性,这些特性使得 Lisp 更像 “Lisp” 。并展示一些 Lisp 能让你完成的新事情。比如说宏: Lisp 程序员能够并且经常编写一些能够写程序的程序。对于程序生成程序这种特性,因为 Lisp 是主流语言中唯一一个提供了相关抽象使得你能够方便地实现这种特性的编程语言,所以 Lisp 是主流语言中唯一一个广泛运用这个特性的语言。我非常乐意邀请那些想要更进一步了解宏和其他高级 Lisp 技术的读者,读一下本书的姐妹篇: On Lisp 。 Tip On Lisp 已经由知名 Lisp 黑客 ── 田春 ── 翻译完成,可以在网络上找到。 ── 田春(知名 Lisp 黑客、Practical Common Lisp 译者) 鸣谢¶在所有帮助我完成这本的朋友当中,我想特别的感谢一下 Robert Morris 。他的重要影响反应在整本书中。他的良好影响使这本书更加优秀。本书中好一些实例程序都源自他手。这些程序包括 138 页的 Henley 和 249 页的模式匹配器。 我很高兴能有一个高水平的技术审稿小组:Skona Brittain, John Foderaro, Nick Levine, Peter Norvig 和 Dave Touretzky。本书中几乎所有部分都得益于它们的意见。 John Foderaro 甚至重写了本书 5.7 节中一些代码。 另外一些人通篇阅读了本书的手稿,它们是:Ken Anderson, Tom Cheatham, Richard Fateman, Steve Hain, Barry Margolin, Waldo Pacheco, Wheeler Ruml 和 Stuart Russell。特别要提一下,Ken Anderson 和 Wheeler Ruml 给予了很多有用的意见。 我非常感谢 Cheatham 教授,更广泛的说,哈佛,提供我编写这本书的一些必要条件。另外也要感谢 Aiken 实验室的人员:Tony Hartman, Dave Mazieres, Janusz Juda, Harry Bochner 和 Joanne Klys。 我非常高兴能再一次有机会和 Alan Apt 合作。还有这些在 Prentice Hall 工作的人士: Alan, Mona, Pompili Shirley McGuire 和 Shirley Michaels, 能与你们共事我很高兴。 本书用 Leslie Lamport 写的 LaTeX 进行排版。LaTeX 是在 Donald Knuth 编写的 TeX 的基础上,又加了 L.A.Carr, Van Jacobson 和 Guy Steele 所编写的宏完成。书中的图表是由 John Vlissides 和 Scott Stanton 编写的 Idraw 完成的。整本书的预览是由 Tim Theisen 写的 Ghostview 完成的。 Ghostview 是根据 L. Peter Deutsch 的 Ghostscript 创建的。 我还需要感谢其他的许多人,包括:Henry Baker, Kim Barrett, Ingrid Bassett, Trevor Blackwell, Paul Becker, Gary Bisbee, Frank Deutschmann, Frances Dickey, Rich 和 Scott Draves, Bill Dubuque, Dan Friedman, Jenny Graham, Alice Hartley, David Hendler, Mike Hewett, Glenn Holloway, Brad Karp, Sonya Keene, Ross Knights, Mutsumi Komuro, Steffi Kutzia, David Kuznick, Madi Lord, Julie Mallozzi, Paul McNamee, Dave Moon, Howard Mullings, Mark Nitzberg, Nancy Parmet 和其家人, Robert Penny, Mike Plusch, Cheryl Sacks, Hazem Sayed, Shannon Spires, Lou Steinberg, Paul Stoddard, John Stone, Guy Steele, Steve Strassmann, Jim Veitch, Dave Watkins, Idelle and Julian Weber, the Weickers, Dave Yost 和 Alan Yuille。 另外,着重感谢我的父母和 Jackie。 高德纳给他的经典丛书起名为《计算机程序设计艺术》。在他的图灵奖获奖感言中,他解释说这本书的书名源自于内心深处的潜意识 ── 潜意识告诉他,编程其实就是追求编写最优美的程序。 就像建筑设计一样,编程既是一门工程技艺也是一门艺术。一个程序要遵循数学原理也要符合物理定律。但是建筑师的目的不仅仅是建一个不会倒塌的建筑。更重要的是,他们要建一个优美的建筑。 像高德纳一样,很多程序员认为编程的真正目的,不仅仅是编写出正确的程序,更重要的是写出优美的代码。几乎所有的 Lisp 黑客也是这么想的。 Lisp 黑客精神可以用两句话来概括:编程应该是有趣的。程序应该是优美的。这就是我在这本书中想要传达的精神。 第一章:简介¶约翰麦卡锡和他的学生于 1958 年展开 Lisp 的初次实现工作。 Lisp 是继 FORTRAN 之后,仍在使用的最古老的程序语言。 λ 更值得注意的是,它仍走在程序语言技术的最前面。懂 Lisp 的程序员会告诉你,有某种东西使 Lisp 与众不同。 Lisp 与众不同的部分原因是,它被设计成能够自己进化。你能用 Lisp 定义新的 Lisp 操作符。当新的抽象概念风行时(如面向对象程序设计),我们总是发现这些新概念在 Lisp 是最容易来实现的。Lisp 就像生物的 DNA 一样,这样的语言永远不会过时。 1.1 新的工具 (New Tools)¶为什么要学 Lisp?因为它让你能做一些其它语言做不到的事情。如果你只想写一个函数来返回小于 ; Lisp /* C */
(defun sum (n) int sum(int n){
(let ((s 0)) int i, s = 0;
(dotimes (i n s) for(i = 0; i < n; i++)
(incf s i)))) s += i;
return(s);
}
如果你只想做这种简单的事情,那用什么语言都不重要。假设你想写一个函数,输入一个数 ; Lisp
(defun addn (n)
#'(lambda (x)
(+ x n)))
在 C 语言中 你可能会想,谁会想做这样的事情?程序语言教你不要做它们没有提供的事情。你得针对每个程序语言,用其特定的思维来写程序,而且想得到你所不能描述的东西是很困难的。当我刚开始编程时 ── 用 Baisc ── 我不知道什么是递归,因为我根本不知道有这个东西。我是用 Basic 在思考。我只能用迭代的概念表达算法,所以我怎么会知道递归呢? 如果你没听过词法闭包 「Lexical Closure」 (上述 闭包仅是其中一个我们在别的语言找不到的抽象概念之一。另一个更有价值的 Lisp 特点是, Lisp 程序是用 Lisp 的数据结构来表示。这表示你可以写出会写程序的程序。人们真的需要这个吗?没错 ── 它们叫做宏,有经验的程序员也一直在使用它。学到 173 页你就可以自己写出自己的宏了。 有了宏、闭包以及运行期类型,Lisp 凌驾在面向对象程序设计之上。如果你了解上面那句话,也许你不应该阅读此书。你得充分了解 Lisp 才能明白为什么此言不虚。但这不是空泛之言。这是一个重要的论点,并且在 17 章用程序相当明确的证明了这点。 第二章到第十三章会循序渐进地介绍所有你需要理解第 17 章程序的概念。你的努力会有所回报:你会感到在 C++ 编程是窒碍难行的,就像有经验的 C++ 程序员用 Basic 编程会感到窒息一样。更加鼓舞人心的是,如果我们思考为什么会有这种感觉。 编写 Basic 对于平常用 C++ 编程是令人感到窒息的,是因为有经验的 C++ 程序员知道一些用 Basic 不可能表达出来的技术。同样地,学习 Lisp 不仅教你学会一门新的语言 ── 它教你崭新的并且更强大的程序思考方法。 1.2 新的技术 (New Techniques)¶如上一节所提到的, Lisp 赋予你别的语言所没有的工具。不仅仅如此,就 Lisp 带来的新特性来说 ── 自动内存管理 (automatic memory management),显式类型 (manifest typing),闭包 (closures)等 ── 每一项都使得编程变得如此简单。结合起来,它们组成了一个关键的部分,使得一种新的编程方式是有可能的。 Lisp 被设计成可扩展的:让你定义自己的操作符。这是可能的,因为 Lisp 是由和你程序一样的函数与宏所构成的。所以扩展 Lisp 就和写一个 Lisp 程序一样简单。事实上,它是如此的容易(和有用),以至于扩展语言自身成了标准实践。当你在用 Lisp 语言編程时,你也在创造一个适合你的程序的语言。你由下而上地,也由上而下地工作。 几乎所有的程序,都可以从订作适合自己所需的语言中受益。然而越复杂的程序,由下而上的程序设计就显得越有价值。一个由下而上所设计出来的程序,可写成一系列的层,每层担任上一层的程序语言。 TeX 是最早使用这种方法所写的程序之一。你可以用任何语言由下而上地设计程序,但 Lisp 是本质上最适合这种方法的工具。 由下而上的编程方法,自然发展出可扩展的软件。如果你把由下而上的程序设计的原则,想成你程序的最上层,那这层就成为使用者的程序语言。正因可扩展的思想深植于 Lisp 当中,使得 Lisp 成为实现可扩展软件的理想语言。三个 1980 年代最成功的程序提供 Lisp 作为扩展自身的语言: GNU Emacs , Autocad ,和 Interleaf 。 由下而上的编程方法,也是得到可重用软件的最好方法。写可重用软件的本质是把共同的地方从细节中分离出来,而由下而上的编程方法本质地创造这种分离。与其努力撰写一个庞大的应用,不如努力创造一个语言,用相对小的努力在这语言上撰写你的应用。和应用相关的特性集中在最上层,以下的层可以组成一个适合这种应用的语言 ── 还有什么比程序语言更具可重用性的呢? Lisp 让你不仅编写出更复杂的程序,而且写的更快。 Lisp 程序通常很简短 ── Lisp 给了你更高的抽象化,所以你不用写太多代码。就像 Frederick Brooks 所指出的,编程所花的时间主要取决于程序的长度。因此仅仅根据这个单独的事实,就可以推断出用 Lisp 编程所花的时间较少。这种效果被 Lisp 的动态特点放大了:在 Lisp 中,编辑-编译-测试循环短到使编程像是即时的。 更高的抽象化与互动的环境,能改变各个机构开发软件的方式。术语快速建型描述了一种始于 Lisp 的编程方法:在 Lisp 里,你可以用比写规格说明更短的时间,写一个原型出来,而这种原型是高度抽象化的,可作为一个比用英语所写的更好的规格说明。而且 Lisp 让你可以轻易的从原型转成产品软件。当写一个考虑到速度的 Common Lisp 程序时,通过现代编译器的编译,Lisp 与其他的高阶语言所写的程序运行得一样快。 除非你相当熟悉 Lisp ,这个简介像是无意义的言论和冠冕堂皇的声明。Lisp 凌驾面向对象程序设计? 你创造适合你程序的语言? Lisp 编程是即时的? 这些说法是什么意思?现在这些说法就像是枯竭的湖泊。随着你学到更多实际的 Lisp 特色,见过更多可运行的程序,这些说法就会被实际经验之水所充满,而有了明确的形状。 1.3 新的方法 (New Approach)¶本书的目标之一是不仅是教授 Lisp 语言,而是教授一种新的编程方法,这种方法因为有了 Lisp 而有可能实现。这是一种你在未来会见得更多的方法。随着开发环境变得更强大,程序语言变得更抽象, Lisp 的编程风格正逐渐取代旧的规划-然后-实现 (plan-and-implement)的模式。 在旧的模式中,错误永远不应该出现。事前辛苦订出缜密的规格说明,确保程序完美的运行。理论上听起来不错。不幸地,规格说明是人写的,也是人来实现的。实际上结果是, 规划-然后-实现 模型不太有效。 身为 OS/360 的项目经理, Frederick Brooks 非常熟悉这种传统的模式。他也非常熟悉它的后果:
而这却描述了那个时代最成功系统之一。 旧模式的问题是它忽略了人的局限性。在旧模式中,你打赌规格说明不会有严重的缺失,实现它们不过是把规格转成代码的简单事情。经验显示这实在是非常坏的赌注。打赌规格说明是误导的,程序到处都是臭虫 (bug) 会更保险一点。 这其实就是新的编程模式所假设的。设法尽量降低错误的成本,而不是希望人们不犯错。错误的成本是修补它所花费的时间。使用强大的语言跟好的开发环境,这种成本会大幅地降低。编程风格可以更多地依靠探索,较少地依靠事前规划。 规划是一种必要之恶。它是评估风险的指标:越是危险,预先规划就显得更重要。强大的工具降低了风险,也降低了规划的需求。程序的设计可以从最有用的信息来源中受益:过去实作程序的经验。 Lisp 风格从 1960 年代一直朝着这个方向演进。你在 Lisp 中可以如此快速地写出原型,以致于你已历经好几个设计和实现的循环,而在旧的模式当中,你可能才刚写完规格说明。你不必担心设计的缺失,因为你将更快地发现它们。你也不用担心有那么多臭虫。当你用函数式风格来编程,你的臭虫只有局部的影响。当你使用一种很抽象的语言,某些臭虫(如迷途指针)不再可能发生,而剩下的臭虫很容易找出,因为你的程序更短了。当你有一个互动的开发环境,你可以即时修补臭虫,不必经历 编辑,编译,测试的漫长过程。 Lisp 风格会这么演进是因为它产生的结果。听起来很奇怪,少的规划意味著更好的设计。技术史上相似的例子不胜枚举。一个相似的变革发生在十五世纪的绘画圈里。在油画流行前,画家使用一种叫做蛋彩的材料来作画。蛋彩不能被混和或涂掉。犯错的代价非常高,也使得画家变得保守。后来随着油画颜料的出现,作画风格有了大幅地改变。油画“允许你再来一次”这对困难主题的处理,像是画人体,提供了决定性的有利条件。 新的材料不仅使画家更容易作画了。它使新的更大胆的作画方式成为可能。 Janson 写道:
做为一种介质,蛋彩与油画颜料一样美丽。但油画颜料的弹性给想像力更大的发挥空间 ── 这是决定性的因素。 程序设计正经历着相同的改变。新的介质像是“动态的面向对象语言” ── 即 Lisp 。这不是说我们所有的软件在几年内都要用 Lisp 来写。从蛋彩到油画的转变也不是一夜完成的;油彩一开始只在领先的艺术中心流行,而且经常混合着蛋彩来使用。我们现在似乎正处于这个阶段。 Lisp 被大学,研究室和某些顶尖的公司所使用。同时,从 Lisp 借鉴的思想越来越多地出现在主流语言中:交互式编程环境 (interactive programming environment)、垃圾回收(garbage collection)、运行期类型 (run-time typing),仅举其中几个。 强大的工具正降低探索的风险。这对程序员来说是好消息,因为意味者我们可以从事更有野心的项目。油画的确有这个效果。采用油画后的时期正是绘画的黄金时期。类似的迹象正在程序设计的领域中发生。 第二章:欢迎来到 Lisp¶本章的目的是让你尽快开始编程。本章结束时,你会掌握足够多的 Common Lisp 知识来开始写程序。 2.1 形式 (Form)¶人可以通过实践来学习一件事,这对于 Lisp 来说特别有效,因为 Lisp 是一门交互式的语言。任何 Lisp 系统都含有一个交互式的前端,叫做顶层(toplevel)。你在顶层输入 Lisp 表达式,而系统会显示它们的值。 Lisp 通常会打印一个提示符告诉你,它正在等待你的输入。许多 Common Lisp 的实现用 一个最简单的 Lisp 表达式是整数。如果我们在提示符后面输入 > 1
1
>
系统会打印出它的值,接着打印出另一个提示符,告诉你它在等待更多的输入。 在这个情况里,打印的值与输入的值相同。数字 > (+ 2 3)
5
在表达式 在日常生活中,我们会把表达式写作 举例来说,我们想把三个数加起来,用日常生活的表示法,要写两次 2 + 3 + 4
而在 Lisp 里,只需要增加一个实参: (+ 2 3 4)
日常生活中用 > (+)
0
> (+ 2)
2
> (+ 2 3)
5
> (+ 2 3 4)
9
> (+ 2 3 4 5)
14
由于操作符可接受不定数量的实参,我们需要用括号来标明表达式的开始与结束。 表达式可以嵌套。即表达式里的实参,可以是另一个复杂的表达式: > (/ (- 7 1) (- 4 2))
3
上面的表达式用中文来说是, (七减一) 除以 (四减二) 。 Lisp 表示法另一个美丽的地方是:它就是如此简单。所有的 Lisp 表达式,要么是 2 (+ 2 3) (+ 2 3 4) (/ (- 7 1) (- 4 2))
稍后我们将理解到,所有的 Lisp 程序都采用这种形式。而像是 C 这种语言,有着更复杂的语法:算术表达式采用中序表示法;函数调用采用某种前序表示法,实参用逗号隔开;表达式用分号隔开;而一段程序用大括号隔开。 在 Lisp 里,我们用单一的表示法,来表达所有的概念。 2.2 求值 (Evaluation)¶上一小节中,我们在顶层输入表达式,然后 Lisp 显示它们的值。在这节里我们深入理解一下表达式是如何被求值的。 在 Lisp 里, 当 Lisp 对函数调用求值时,它做下列两个步骤:
如果实参本身是函数调用的话,上述规则同样适用。以下是当
但不是所有的 Common Lisp 操作符都是函数,不过大部分是。函数调用都是这么求值。由左至右对实参求值,将它们的数值传入函数,来返回整个表达式的值。这称为 Common Lisp 的求值规则。 Note 逃离麻烦 如果你试着输入 Lisp 不能理解的东西,它会打印一个错误讯息,接著带你到一种叫做中断循环(break loop)的顶层。
中断循环给予有经验的程序员一个机会,来找出错误的原因,不过最初你只会想知道如何从中断循环中跳出。
如何返回顶层取决于你所使用的 Common Lisp 实现。在这个假定的实现环境中,输入 > (/ 1 0)
Error: Division by zero
Options: :abort, :backtrace
>> :abort
>
附录 A 演示了如何调试 Lisp 程序,并给出一些常见的错误例子。 一个不遵守 Common Lisp 求值规则的操作符是 > (quote (+ 3 5))
(+ 3 5)
为了方便起见,Common Lisp 定义 > '(+ 3 5)
(+ 3 5)
使用缩写 Lisp 提供 2.3 数据 (Data)¶Lisp 提供了所有在其他语言找的到的,以及其他语言所找不到的数据类型。一个我们已经使用过的类型是整数(integer),整数用一系列的数字来表示,比如:
有两个通常在别的语言所找不到的 Lisp 数据类型是符号(symbol)与列表(lists),符号是英语的单词 (words)。无论你怎么输入,通常会被转换为大写: > 'Artichoke
ARTICHOKE
符号(通常)不对自身求值,所以要是想引用符号,应该像上例那样用 列表是由被括号包住的零个或多个元素来表示。元素可以是任何类型,包含列表本身。使用列表必须要引用,不然 Lisp 会以为这是个函数调用: > '(my 3 "Sons")
(MY 3 "Sons")
> '(the list (a b c) has 3 elements)
(THE LIST (A B C) HAS 3 ELEMENTS)
注意引号保护了整个表达式(包含内部的子表达式)被求值。 你可以调用 > (list 'my (+ 2 1) "Sons")
(MY 3 "Sons")
我们现在来到领悟 Lisp 最卓越特性的地方之一。Lisp的程序是用列表来表示的。如果实参的优雅与弹性不能说服你 Lisp 表示法是无价的工具,这里应该能使你信服。这代表着 Lisp 程序可以写出 Lisp 代码。 Lisp 程序员可以(并且经常)写出能为自己写程序的程序。 不过得到第 10 章,我们才来考虑这种程序,但现在了解到列表和表达式的关系是非常重要的,而不是被它们搞混。这也就是为什么我们需要 > (list '(+ 2 1) (+ 2 1))
((+ 2 1) 3)
这里第一个实参被引用了,所以产生一个列表。第二个实参没有被引用,视为函数调用,经求值后得到一个数字。 在 Common Lisp 里有两种方法来表示空列表。你可以用一对不包括任何东西的括号来表示,或用符号 > ()
NIL
> nil
NIL
你不需要引用 2.4 列表操作 (List Operations)¶用函数 > (cons 'a '(b c d))
(A B C D)
可以通过把新元素建立在空表之上,来构造一个新列表。上一节所看到的函数 > (cons 'a (cons 'b nil))
(A B)
> (list 'a 'b)
(A B)
取出列表元素的基本函数是 > (car '(a b c))
A
> (cdr '(a b c))
(B C)
你可以把 > (car (cdr (cdr '(a b c d))))
C
不过,你可以用更简单的 > (third '(a b c d))
C
2.5 真与假 (Truth)¶在 Common Lisp 里,符号 > (listp '(a b c))
T
函数的返回值将会被解释成逻辑 逻辑 > (listp 27)
NIL
由于 > (null nil)
T
而如果实参是逻辑 > (not nil)
T
在 Common Lisp 里,最简单的条件式是 > (if (listp '(a b c))
(+ 1 2)
(+ 5 6))
3
> (if (listp 27)
(+ 1 2)
(+ 5 6))
11
与 > (if (listp 27)
(+ 1 2))
NIL
虽然 > (if 27 1 2)
1
逻辑操作符 > (and t (+ 1 2))
3
如果其中一个实参为 以上这两个操作符称为宏。宏和特殊的操作符一样,可以绕过一般的求值规则。第十章解释了如何编写你自己的宏。 2.6 函数 (Functions)¶你可以用 > (defun our-third (x)
(car (cdr (cdr x))))
OUR-THIRD
第一个实参说明此函数的名称将是 定义的剩余部分, > (our-third '(a b c d))
C
既然我们已经讨论过了变量,理解符号是什么就更简单了。符号是变量的名字,符号本身就是以对象的方式存在。这也是为什么符号,必须像列表一样被引用。列表必须被引用,不然会被视为代码。符号必须要被引用,不然会被当作变量。 你可以把函数定义想成广义版的 Lisp 表达式。下面的表达式测试 > (> (+ 1 4) 3)
T
通过将这些数字替换为变量,我们可以写个函数,测试任两数之和是否大于第三个数: > (defun sum-greater (x y z)
(> (+ x y) z))
SUM-GREATER
> (sum-greater 1 4 3)
T
Lisp 不对程序、过程以及函数作区别。函数做了所有的事情(事实上,函数是语言的主要部分)。如果你想要把你的函数之一作为主函数(main function),可以这么做,但平常你就能在顶层中调用任何函数。这表示当你编程时,你可以把程序拆分成一小块一小块地来做调试。 2.7 递归 (Recursion)¶上一节我们所定义的函数,调用了别的函数来帮它们做事。比如 > (defun our-member (obj lst)
(if (null lst)
nil
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst)))))
OUR-MEMBER
谓词 > (our-member 'b '(a b c))
(B C)
> (our-member 'z '(a b c))
NIL
下面是
当你想要了解递归函数是怎么工作时,把它翻成这样的叙述有助于你理解。 起初,许多人觉得递归函数很难理解。大部分的理解难处,来自于对函数使用了错误的比喻。人们倾向于把函数理解为某种机器。原物料像实参一样抵达;某些工作委派给其它函数;最后组装起来的成品,被作为返回值运送出去。如果我们用这种比喻来理解函数,那递归就自相矛盾了。机器怎可以把工作委派给自己?它已经在忙碌中了。 较好的比喻是,把函数想成一个处理的过程。在过程里,递归是在自然不过的事情了。日常生活中我们经常看到递归的过程。举例来说,假设一个历史学家,对欧洲历史上的人口变化感兴趣。研究文献的过程很可能是:
过程是很容易理解的,而且它是递归的,因为第三个步骤可能带出一个或多个同样的过程。 所以,别把 2.8 阅读 Lisp (Reading Lisp)¶上一节我们所定义的 答案是,你不需要这么做。 Lisp 程序员用缩排来阅读及编写程序,而不是括号。当他们在写程序时,他们让文字编辑器显示哪个括号该与哪个匹配。任何好的文字编辑器,特别是 Lisp 系统自带的,都应该能做到括号匹配(paren-matching)。在这种编辑器中,当你输入一个括号时,编辑器指出与其匹配的那一个。如果你的编辑器不能匹配括号,别用了,想想如何让它做到,因为没有这个功能,你根本不可能编 Lisp 程序 [1] 。 有了好的编辑器之后,括号匹配不再会是问题。而且由于 Lisp 缩排有通用的惯例,阅读程序也不是个问题。因为所有人都使用一样的习惯,你可以忽略那些括号,通过缩排来阅读程序。 任何有经验的 Lisp 黑客,会发现如果是这样的 (defun our-member (obj lst) (if (null lst) nil (if
(eql (car lst) obj) lst (our-member obj (cdr lst)))))
但如果程序适当地缩排时,他就没有问题了。可以忽略大部分的括号而仍能读懂它: defun our-member (obj lst)
if null lst
nil
if eql (car lst) obj
lst
our-member obj (cdr lst)
事实上,这是你在纸上写 Lisp 程序的实用方法。等输入程序至计算机的时候,可以利用编辑器匹配括号的功能。 2.9 输入输出 (Input and Output)¶到目前为止,我们已经利用顶层偷偷使用了 I/O 。对实际的交互程序来说,这似乎还是不太够。在这一节,我们来看几个输入输出的函数。 最普遍的 Common Lisp 输出函数是 > (format t "~A plus ~A equals ~A. ~%" 2 3 (+ 2 3))
2 plus 3 equals 5.
NIL
注意到有两个东西被打印出来。第一行是
标准的输入函数是 (defun askem (string)
(format t "~A" string)
(read))
它的行为如下: > (askem "How old are you?")
How old are you?29
29
记住 第二件关于
在之前的每一节中,我们坚持所谓“纯粹的” Lisp ── 即没有副作用的 Lisp 。副作用是指,表达式被求值后,对外部世界的状态做了某些改变。当我们对一个如 当我们想要写没有副作用的程序时,则定义多个表达式的函数主体就没有意义了。最后一个表达式的值,会被当成函数的返回值,而之前表达式的值都被舍弃了。如果这些表达式没有副作用,你没有任何理由告诉 Lisp ,为什么要去对它们求值。 2.10 变量 (Variables)¶
> (let ((x 1) (y 2))
(+ x y))
3
一个 一组变量与数值之后,是一个有表达式的函数体,表达式依序被求值。但这个例子里,只有一个表达式,调用 (defun ask-number ()
(format t "Please enter a number. ")
(let ((val (read)))
(if (numberp val)
val
(ask-number))))
这个函数创建了变量 如果使用者不是输入一个数字, > (ask-number)
Please enter a number. a
Please enter a number. (ho hum)
Please enter a number. 52
52
我们已经看过的这些变量都叫做局部变量。它们只在特定的上下文里有效。另外还有一种变量叫做全局变量(global variable),是在任何地方都是可视的。 [2] 你可以给 > (defparameter *glob* 99)
*GLOB*
全局变量在任何地方都可以存取,除了在定义了相同名字的区域变量的表达式里。为了避免这种情形发生,通常我们在给全局变量命名时,以星号作开始与结束。刚才我们创造的变量可以念作 “星-glob-星” (star-glob-star)。 你也可以用 (defconstant limit (+ *glob* 1))
我们不需要给常量一个独一无二的名字,因为如果有相同名字存在,就会有错误产生 (error)。如果你想要检查某些符号,是否为一个全局变量或常量,使用 > (boundp '*glob*)
T
2.11 赋值 (Assignment)¶在 Common Lisp 里,最普遍的赋值操作符(assignment operator)是 > (setf *glob* 98)
98
> (let ((n 10))
(setf n 2)
n)
2
如果 > (setf x (list 'a 'b 'c))
(A B C)
也就是说,通过赋值,你可以隐式地创建全局变量。
不过,一般来说,还是使用 你不仅可以给变量赋值。传入 > (setf (car x) 'n)
N
> x
(N B C)
(setf a 'b
c 'd
e 'f)
等同于依序调用三个单独的 (setf a 'b)
(setf c 'd)
(setf e 'f)
2.12 函数式编程 (Functional Programming)¶函数式编程意味着撰写利用返回值而工作的程序,而不是修改东西。它是 Lisp 的主流范式。大部分 Lisp 的内置函数被调用是为了取得返回值,而不是副作用。 举例来说,函数 > (setf lst '(c a r a t))
(C A R A T)
> (remove 'a lst)
(C R T)
为什么不干脆说 > lst
(C A R A T)
若你真的想从列表里移除某些东西怎么办?在 Lisp 通常你这么做,把这个列表当作实参,传入某个函数,并使用 (setf x (remove 'a x))
函数式编程本质上意味着避免使用如 完全不用到副作用是很不方便的。然而,随着你进一步阅读,会惊讶地发现需要用到副作用的地方很少。副作用用得越少,你就更上一层楼。 函数式编程最重要的优点之一是,它允许交互式测试(interactive testing)。在纯函数式的程序里,你可以测试每个你写的函数。如果它返回你预期的值,你可以有信心它是对的。这额外的信心,集结起来,会产生巨大的差别。当你改动了程序里的任何一个地方,会得到即时的改变。而这种即时的改变,使我们有一种新的编程风格。类比于电话与信件,让我们有一种新的通讯方式。 2.13 迭代 (Iteration)¶当我们想重复做一些事情时,迭代比递归来得更自然。典型的例子是用迭代来产生某种表格。这个函数 (defun show-squares (start end)
(do ((i start (+ i 1)))
((> i end) 'done)
(format t "~A ~A~%" i (* i i))))
列印从 > (show-squares 2 5)
2 4
3 9
4 16
5 25
DONE
(variable initial update)
其中 variable 是一个符号, initial 和 update 是表达式。最初每个变量会被赋予 initial 表达式的值;每一次迭代时,会被赋予 update 表达式的值。在 第二个传给
作为对比,以下是递归版本的 (defun show-squares (i end)
(if (> i end)
'done
(progn
(format t "~A ~A~%" i (* i i))
(show-squares (+ i 1) end))))
唯一的新东西是 为了处理某些特殊情况, Common Lisp 有更简单的迭代操作符。举例来说,要遍历列表的元素,你可能会使用 (defun our-length (lst)
(let ((len 0))
(dolist (obj lst)
(setf len (+ len 1)))
len))
这里 (defun our-length (lst)
(if (null lst)
0
(+ (our-length (cdr lst)) 1)))
也就是说,如果列表是空表,则长度为 2.14 函数作为对象 (Functions as Objects)¶函数在 Lisp 里,和符号、字符串或列表一样,是稀松平常的对象。如果我们把函数的名字传给 > (function +)
#<Compiled-Function + 17BA4E>
这看起来很奇怪的返回值,是在典型的 Common Lisp 实现里,函数可能的打印表示法。 到目前为止,我们仅讨论过,不管是 Lisp 打印它们,还是我们输入它们,看起来都是一样的对象。但这个惯例对函数不适用。一个像是 如同我们可以用 > #'+
#<Compiled-Function + 17BA4E>
这个缩写称之为升引号(sharp-quote)。 和别种对象类似,可以把函数当作实参传入。有个接受函数作为实参的函数是 > (apply #'+ '(1 2 3))
6
> (+ 1 2 3)
6
> (apply #'+ 1 2 '(3 4 5))
15
函数 > (funcall #'+ 1 2 3)
6
Note 什么是
在 Common Lisp 里,你可以用列表来表达函数, 函数在内部会被表示成独特的函数对象。因此不再需要 lambda 了。 如果需要把函数记为
而不是
也是可以的。 但 Lisp 程序员习惯用符号
要直接引用整数,我们使用一系列的数字;要直接引用一个函数,我们使用所谓的lambda 表达式。一个 下面的 (lambda (x y)
(+ x y))
列表 一个 > ((lambda (x) (+ x 100)) 1)
101
而通过在 > (funcall #'(lambda (x) (+ x 100))
1)
2.15 类型 (Types)¶Lisp 处理类型的方法非常灵活。在很多语言里,变量是有类型的,得声明变量的类型才能使用它。在 Common Lisp 里,数值才有类型,而变量没有。你可以想像每个对象,都贴有一个标明其类型的标签。这种方法叫做显式类型(manifest typing)。你不需要声明变量的类型,因为变量可以存放任何类型的对象。 虽然从来不需要声明类型,但出于效率的考量,你可能会想要声明变量的类型。类型声明在第 13.3 节时讨论。 Common Lisp 的内置类型,组成了一个类别的层级。对象总是不止属于一个类型。举例来说,数字 27 的类型,依普遍性的增加排序,依序是 函数 > (typep 27 'integer)
T
我们会在遇到各式内置类型时来讨论它们。 2.16 展望 (Looking Forward)¶本章仅谈到 Lisp 的表面。然而,一种非比寻常的语言形象开始出现了。首先,这个语言用单一的语法,来表达所有的程序结构。语法基于列表,列表是一种 Lisp 对象。函数本身也是 Lisp 对象,函数能用列表来表示。而 Lisp 本身就是 Lisp 程序。几乎所有你定义的函数,与内置的 Lisp 函数没有任何区别。 如果你对这些概念还不太了解,不用担心。 Lisp 介绍了这么多新颖的概念,在你能驾驭它们之前,得花时间去熟悉它们。不过至少要了解一件事:在这些概念当中,有着优雅到令人吃惊的概念。 Richard Gabriel 曾经半开玩笑的说, C 是拿来写 Unix 的语言。我们也可以说, Lisp 是拿来写 Lisp 的语言。但这是两种不同的论述。一个可以用自己编写的语言和一种适合编写某些特定类型应用的语言,是有着本质上的不同。这开创了新的编程方法:你不但在语言之中编程,还把语言改善成适合程序的语言。如果你想了解 Lisp 编程的本质,理解这个概念是个好的开始。 Chapter 2 总结 (Summary)¶
Chapter 2 习题 (Exercises)¶
(a) (+ (- 5 1) (+ 3 7))
(b) (list 1 (+ 2 3))
(c) (if (listp 1) (+ 1 2) (+ 3 4))
(d) (list (and (listp 3) t) (+ 1 2))
(a) (defun enigma (x)
(and (not (null x))
(or (null (car x))
(enigma (cdr x)))))
(b) (defun mystery (x y)
(if (null y)
nil
(if (eql (car y) x)
0
(let ((z (mystery x (cdr y))))
(and z (+ z 1))))))
(a) > (car (x (cdr '(a (b c) d))))
B
(b) > (x 13 (/ 1 0))
13
(c) > (x #'list 1 nil)
(1)
(a) (defun summit (lst)
(remove nil lst)
(apply #'+ lst))
(b) (defun summit (lst)
(let ((x (car lst)))
(if (null x)
(summit (cdr lst))
(+ x (summit (cdr lst))))))
脚注
第三章:列表¶列表是 Lisp 的基本数据结构之一。在最早的 Lisp 方言里,列表是唯一的数据结构: “Lisp” 这个名字起初是 “LISt Processor” 的缩写。但 Lisp 已经超越这个缩写很久了。 Common Lisp 是一个有着各式各样数据结构的通用性程序语言。 Lisp 程序开发通常呼应着开发 Lisp 语言自身。在最初版本的 Lisp 程序,你可能使用很多列表。然而之后的版本,你可能换到快速、特定的数据结构。本章描述了你可以用列表所做的很多事情,以及使用它们来演示一些普遍的 Lisp 概念。 3.1 构造 (Conses)¶在 2.4 节我们介绍了 Cons 对象提供了一个方便的表示法,来表示任何类型的对象。一个 Cons 对象里的一对指针,可以指向任何类型的对象,包括 Cons 对象本身。它利用到我们之后可以用 我们往往不会把列表想成是成对的,但它们可以这样被定义。任何非空的列表,都可以被视为一对由列表第一个元素及列表其余元素所组成的列表。 Lisp 列表体现了这个概念。我们使用 Cons 的一半来指向列表的第一个元素,然后用另一半指向列表其余的元素(可能是别的 Cons 或 当我们想在 > (setf x (cons 'a nil))
(A)
![]() 图 3.1 一个元素的列表 产生的列表由一个 Cons 所组成,见图 3.1。这种表达 Cons 的方式叫做箱子表示法 (box notation),因为每一个 Cons 是用一个箱子表示,内含一个 > (car x)
A
> (cdr x)
NIL
当我们构造一个多元素的列表时,我们得到一串 Cons (a chain of conses): > (setf y (list 'a 'b 'c))
(A B C)
产生的结构见图 3.2。现在当我们想得到列表的 ![]() 图 3.2 三个元素的列表 > (cdr y)
(B C)
在一个有多个元素的列表中, 一个列表可以有任何类型的对象作为元素,包括另一个列表: > (setf z (list 'a (list 'b 'c) 'd))
(A (B C) D)
当这种情况发生时,它的结构如图 3.3 所示;第二个 Cons 的 > (car (cdr z))
(B C)
![]() 图 3.3 嵌套列表 前两个我们构造的列表都有三个元素;只不过 如果参数是一个 Cons 对象,函数 (defun our-listp (x)
(or (null x) (consp x)))
因为所有不是 Cons 对象的东西,就是一个原子 (atom),判断式 (defun our-atom (x) (not (consp x)))
注意, 3.2 等式 (Equality)¶每一次你调用 > (eql (cons 'a nil) (cons 'a nil))
NIL
如果我们也可以询问两个列表是否有相同元素,那就很方便了。 Common Lisp 提供了这种目的另一个判断式: > (setf x (cons 'a nil))
(A)
> (eql x x)
T
本质上 > (equal x (cons 'a nil))
T
这个判断式对非列表结构的别种对象也有效,但一种仅对列表有效的版本可以这样定义: > (defun our-equal (x y)
(or (eql x y)
(and (consp x)
(consp y)
(our-equal (car x) (car y))
(our-equal (cdr x) (cdr y)))))
这个定义意味着,如果某个 勘误: 这个版本的 3.3 为什么 Lisp 没有指针 (Why Lisp Has No Pointers)¶一个理解 Lisp 的秘密之一是意识到变量是有值的,就像列表有元素一样。如同 Cons 对象有指针指向他们的元素,变量有指针指向他们的值。 你可能在别的语言中使用过显式指针 (explicitly pointer)。在 Lisp,你永远不用这么做,因为语言帮你处理好指针了。我们已经在列表看过这是怎么实现的。同样的事情发生在变量身上。举例来说,假设我们想要把两个变量设成同样的列表: > (setf x '(a b c))
(A B C)
> (setf y x)
(A B C)
![]() 图 3.4 两个变量设为相同的列表 当我们把 > (eql x y)
T
Lisp 没有指针的原因是因为每一个值,其实概念上来说都是一个指针。当你赋一个值给变量或将这个值存在数据结构中,其实被储存的是指向这个值的指针。当你要取得变量的值,或是存在数据结构中的内容时, Lisp 返回指向这个值的指针。但这都在台面下发生。你可以不加思索地把值放在结构里,或放“在”变量里。 为了效率的原因, Lisp 有时会选择一个折衷的表示法,而不是指针。举例来说,因为一个小整数所需的内存空间,少于一个指针所需的空间,一个 Lisp 实现可能会直接处理这个小整数,而不是用指针来处理。但基本要点是,程序员预设可以把任何东西放在任何地方。除非你声明你不愿这么做,不然你能够在任何的数据结构,存放任何类型的对象,包括结构本身。 3.4 建立列表 (Building Lists)¶![]() 图 3.5 复制的结果 函数 > (setf x '(a b c)
y (copy-list x))
(A B C)
图 3.5 展示出结果的结构; 返回值像是有着相同乘客的新公交。我们可以把 (defun our-copy-list (lst)
(if (atom lst)
lst
(cons (car lst) (our-copy-list (cdr lst)))))
这个定义暗示着 最后,函数 > (append '(a b) '(c d) 'e)
(A B C D . E)
通过这么做,它复制所有的参数,除了最后一个 3.5 示例:压缩 (Example: Compression)¶作为一个例子,这节将演示如何实现简单形式的列表压缩。这个算法有一个令人印象深刻的名字,游程编码(run-length encoding)。 (defun compress (x)
(if (consp x)
(compr (car x) 1 (cdr x))
x))
(defun compr (elt n lst)
(if (null lst)
(list (n-elts elt n))
(let ((next (car lst)))
(if (eql next elt)
(compr elt (+ n 1) (cdr lst))
(cons (n-elts elt n)
(compr next 1 (cdr lst)))))))
(defun n-elts (elt n)
(if (> n 1)
(list n elt)
elt))
图 3.6 游程编码 (Run-length encoding):压缩 在餐厅的情境下,这个算法的工作方式如下。一个女服务生走向有四个客人的桌子。“你们要什么?” 她问。“我要特餐,” 第一个客人说。 “我也是,” 第二个客人说。“听起来不错,” 第三个客人说。每个人看着第四个客人。 “我要一个 cilantro soufflé,” 他小声地说。 (译注:蛋奶酥上面洒香菜跟酱料) 瞬息之间,女服务生就转身踩着高跟鞋走回柜台去了。 “三个特餐,” 她大声对厨师说,“还有一个香菜蛋奶酥。” 图 3.6 展示了如何实现这个压缩列表演算法。函数 > (compress '(1 1 1 0 1 0 0 0 0 1))
((3 1) 0 1 (4 0) 1)
当相同的元素连续出现好几次,这个连续出现的序列 (sequence)被一个列表取代,列表指明出现的次数及出现的元素。 主要的工作是由递归函数 要复原一个压缩的列表,我们调用 > (uncompress '((3 1) 0 1 (4 0) 1))
(1 1 1 0 1 0 0 0 0 1)
(defun uncompress (lst)
(if (null lst)
nil
(let ((elt (car lst))
(rest (uncompress (cdr lst))))
(if (consp elt)
(append (apply #'list-of elt)
rest)
(cons elt rest)))))
(defun list-of (n elt)
(if (zerop n)
nil
(cons elt (list-of (- n 1) elt))))
图 3.7 游程编码 (Run-length encoding):解压缩 这个函数递归地遍历这个压缩列表,逐字复制原子并调用 > (list-of 3 'ho)
(HO HO HO)
我们其实不需要自己写 图 3.6 跟 3.7 这种写法不是一个有经验的Lisp 程序员用的写法。它的效率很差,它没有尽可能的压缩,而且它只对由原子组成的列表有效。在几个章节内,我们会学到解决这些问题的技巧。 载入程序
在这节的程序是我们第一个实质的程序。
当我们想要写超过数行的函数时,
通常我们会把程序写在一个文件,
然后使用 load 让 Lisp 读取函数的定义。
如果我们把图 3.6 跟 3.7 的程序,
存在一个文件叫做,“compress.lisp”然后输入
(load "compress.lisp")
到顶层,或多或少的,
我们会像在直接输入顶层一样得到同样的效果。
注意:在某些实现中,Lisp 文件的扩展名会是“.lsp”而不是“.lisp”。
3.6 存取 (Access)¶Common Lisp 有额外的存取函数,它们是用 > (nth 0 '(a b c))
A
而要找到第 > (nthcdr 2 '(a b c))
(C)
两个函数几乎做一样的事; (defun our-nthcdr (n lst)
(if (zerop n)
lst
(our-nthcdr (- n 1) (cdr lst))))
函数 函数 > (last '(a b c))
(C)
这跟取得最后一个元素不一样。要取得列表的最后一个元素,你要取得 Common Lisp 定义了函数
此外, Common Lisp 定义了像是 3.7 映射函数 (Mapping Functions)¶Common Lisp 提供了数个函数来对一个列表的元素做函数调用。最常使用的是 > (mapcar #'(lambda (x) (+ x 10))
'(1 2 3))
(11 12 13)
> (mapcar #'list
'(a b c)
'(1 2 3 4))
((A 1) (B 2) (C 3))
相关的 > (maplist #'(lambda (x) x)
'(a b c))
((A B C) (B C) (C))
其它的映射函数,包括 3.8 树 (Trees)¶Cons 对象可以想成是二叉树,
![]() 图 3.8 二叉树 (Binary Tree) Common Lisp 有几个内置的操作树的函数。举例来说, (defun our-copy-tree (tr)
(if (atom tr)
tr
(cons (our-copy-tree (car tr))
(our-copy-tree (cdr tr)))))
把这跟 36 页的 没有内部节点的二叉树没有太大的用处。 Common Lisp 包含了操作树的函数,不只是因为我们需要树这个结构,而是因为我们需要一种方法,来操作列表及所有内部的列表。举例来说,假设我们有一个这样的列表: (and (integerp x) (zerop (mod x 2)))
而我们想要把各处的 > (substitute 'y 'x '(and (integerp x) (zerop (mod x 2))))
(AND (INTEGERP X) (ZEROP (MOD X 2)))
这个调用是无效的,因为列表有三个元素,没有一个元素是 > (subst 'y 'x '(and (integerp x) (zerop (mod x 2))))
(AND (INTEGERP Y) (ZEROP (MOD Y 2)))
如果我们定义一个 > (defun our-subst (new old tree)
(if (eql tree old)
new
(if (atom tree)
tree
(cons (our-subst new old (car tree))
(our-subst new old (cdr tree))))))
操作树的函数通常有这种形式, 3.9 理解递归 (Understanding Recursion)¶学生在学习递归时,有时候是被鼓励在纸上追踪 (trace)递归程序调用 (invocation)的过程。 (288页「译注:附录 A 追踪与回溯」可以看到一个递归函数的追踪过程。)但这种练习可能会误导你:一个程序员在定义一个递归函数时,通常不会特别地去想函数的调用顺序所导致的结果。 如果一个人总是需要这样子思考程序,递归会是艰难的、没有帮助的。递归的优点是它精确地让我们更抽象地来设计算法。你不需要考虑真正函数时所有的调用过程,就可以判断一个递归函数是否是正确的。 要知道一个递归函数是否做它该做的事,你只需要问,它包含了所有的情况吗?举例来说,下面是一个寻找列表长度的递归函数: > (defun len (lst)
(if (null lst)
0
(+ (len (cdr lst)) 1)))
我们可以借由检查两件事情,来确信这个函数是正确的:
如果这两点是成立的,我们知道这个函数对于所有可能的列表都是正确的。 我们的定义显然地满足第一点:如果列表( 我们需要知道的就是这些。理解递归的秘密就像是处理括号一样。你怎么知道哪个括号对上哪个?你不需要这么做。你怎么想像那些调用过程?你不需要这么做。 更复杂的递归函数,可能会有更多的情况需要讨论,但是流程是一样的。举例来说, 41 页的 第一个情况(长度零的列表)称之为基本用例( base case )。当一个递归函数不像你想的那样工作时,通常是处理基本用例就错了。下面这个不正确的 (defun our-member (obj lst)
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst))))
我们需要初始一个 能够判断一个递归函数是否正确只不过是理解递归的上半场,下半场是能够写出一个做你想做的事情的递归函数。 6.9 节讨论了这个问题。 3.10 集合 (Sets)¶列表是表示小集合的好方法。列表中的每个元素都代表了一个集合的成员: > (member 'b '(a b c))
(B C)
当 一般情况下, 一个 如果你在调用 > (member '(a) '((a) (z)) :test #'equal)
((A) (Z))
关键字参数总是选择性添加的。如果你在一个调用中包含了任何的关键字参数,他们要摆在最后; 如果使用了超过一个的关键字参数,摆放的顺序无关紧要。 另一个 > (member 'a '((a b) (c d)) :key #'car)
((A B) (C D))
在这个例子里,我们询问是否有一个元素的 如果我们想要使用两个关键字参数,我们可以使用其中一个顺序。下面这两个调用是等价的: > (member 2 '((1) (2)) :key #'car :test #'equal)
((2))
> (member 2 '((1) (2)) :test #'equal :key #'car)
((2))
两者都询问是否有一个元素的 如果我们想要找到一个元素满足任意的判断式像是── > (member-if #'oddp '(2 3 4))
(3 4)
我们可以想像一个限制性的版本 (defun our-member-if (fn lst)
(and (consp lst)
(if (funcall fn (car lst))
lst
(our-member-if fn (cdr lst)))))
函数 > (adjoin 'b '(a b c))
(A B C)
> (adjoin 'z '(a b c))
(Z A B C)
通常的情况下它接受与 集合论中的并集 (union)、交集 (intersection)以及补集 (complement)的实现,是由函数 这些函数期望两个(正好 2 个)列表(一样接受与 > (union '(a b c) '(c b s))
(A C B S)
> (intersection '(a b c) '(b b c))
(B C)
> (set-difference '(a b c d e) '(b e))
(A C D)
因为集合中没有顺序的概念,这些函数不需要保留原本元素在列表被找到的顺序。举例来说,调用 3.11 序列 (Sequences)¶另一种考虑一个列表的方式是想成一系列有特定顺序的对象。在 Common Lisp 里,序列( sequences )包括了列表与向量 (vectors)。本节介绍了一些可以运用在列表上的序列函数。更深入的序列操作在 4.4 节讨论。 函数 > (length '(a b c))
3
我们在 24 页 (译注:2.13节 要复制序列的一部分,我们使用 > (subseq '(a b c d) 1 2)
(B)
>(subseq '(a b c d) 1)
(B C D)
如果省略了第三个参数,子序列会从第二个参数给定的位置引用到序列尾端。 函数 > (reverse '(a b c))
(C B A)
一个回文 (palindrome) 是一个正读反读都一样的序列 —— 举例来说, (defun mirror? (s)
(let ((len (length s)))
(and (evenp len)
(let ((mid (/ len 2)))
(equal (subseq s 0 mid)
(reverse (subseq s mid)))))))
来检测是否是回文: > (mirror? '(a b b a))
T
Common Lisp 有一个内置的排序函数叫做 > (sort '(0 2 1 3 8) #'>)
(8 3 2 1 0)
你要小心使用 使用 (defun nthmost (n lst)
(nth (- n 1)
(sort (copy-list lst) #'>)))
我们把整数减一因为 (nthmost 2 '(0 2 1 3 8))
多努力一点,我们可以写出这个函数的一个更有效率的版本。 函数 > (every #'oddp '(1 3 5))
T
> (some #'evenp '(1 2 3))
T
如果它们输入多于一个序列时,判断式必须接受与序列一样多的元素作为参数,而参数从所有序列中一次提取一个: > (every #'> '(1 3 5) '(0 2 4))
T
如果序列有不同的长度,最短的那个序列,决定需要测试的次数。 3.12 栈 (Stacks)¶用 Cons 对象来表示的列表,很自然地我们可以拿来实现下推栈 (pushdown stack)。这太常见了,以致于 Common Lisp 提供了两个宏给堆使用: 两个函数都是由 表达式
等同于
而表达式
等同于 (let ((x (car lst)))
(setf lst (cdr lst))
x)
所以,举例来说: > (setf x '(b))
(B)
> (push 'a x)
(A B)
> x
(A B)
> (setf y x)
(A B)
> (pop x)
(A)
> x
(B)
> y
(A B)
以上,全都遵循上述由 ![]() 图 3.9 push 及 pop 的效果 你可以使用 (defun our-reverse (lst)
(let ((acc nil))
(dolist (elt lst)
(push elt acc))
acc))
在这个版本,我们从一个空列表开始,然后把
> (let ((x '(a b)))
(pushnew 'c x)
(pushnew 'a x)
x)
(C A B)
在这里, 3.13 点状列表 (Dotted Lists)¶调用 (defun proper-list? (x)
(or (null x)
(and (consp x)
(proper-list? (cdr x)))))
至目前为止,我们构造的列表都是正规列表。 然而, > (setf pair (cons 'a 'b))
(A . B)
因为这个 Cons 对象不是一个正规列表,它用点状表示法来显示。在点状表示法,每个 Cons 对象的 ![]() 图3.10 一个成对的 Cons 对象 (A cons used as a pair) 一个非正规列表的 Cons 对象称之为点状列表 (dotted list)。这不是个好名字,因为非正规列表的 Cons 对象通常不是用来表示列表: 你也可以用点状表示法表示正规列表,但当 Lisp 显示一个正规列表时,它会使用普通的列表表示法: > '(a . (b . (c . nil)))
(A B C)
顺道一提,注意列表由点状表示法与图 3.2 箱子表示法的关联性。 还有一个过渡形式 (intermediate form)的表示法,介于列表表示法及纯点状表示法之间,对于 > (cons 'a (cons 'b (cons 'c 'd)))
(A B C . D)
![]() 图 3.11 一个点状列表 (A dotted list) 这样的 Cons 对象看起来像正规列表,除了最后一个 cdr 前面有一个句点。这个列表的结构展示在图 3.11 ; 注意它跟图3.2 是多么的相似。 所以实际上你可以这么表示列表 (a . (b . nil))
(a . (b))
(a b . nil)
(a b)
虽然 Lisp 总是使用后面的形式,来显示这个列表。 3.14 关联列表 (Assoc-lists)¶用 Cons 对象来表示映射 (mapping)也是很自然的。一个由 Cons 对象组成的列表称之为关联列表(assoc-listor alist)。这样的列表可以表示一个翻译的集合,举例来说: > (setf trans '((+ . "add") (- . "subtract")))
((+ . "add") (- . "subtract"))
关联列表很慢,但是在初期的程序中很方便。 Common Lisp 有一个内置的函数 > (assoc '+ trans)
(+ . "add")
> (assoc '* trans)
NIL
如果 我们可以定义一个受限版本的 (defun our-assoc (key alist)
(and (consp alist)
(let ((pair (car alist)))
(if (eql key (car pair))
pair
(our-assoc key (cdr alist))))))
和 3.15 示例:最短路径 (Example: Shortest Path)¶图 3.12 包含一个搜索网络中最短路径的程序。函数 在这个范例中,节点用符号表示,而网络用含以下元素形式的关联列表来表示: (node . neighbors) 所以由图 3.13 展示的最小网络 (minimal network)可以这样来表示:
(defun shortest-path (start end net)
(bfs end (list (list start)) net))
(defun bfs (end queue net)
(if (null queue)
nil
(let ((path (car queue)))
(let ((node (car path)))
(if (eql node end)
(reverse path)
(bfs end
(append (cdr queue)
(new-paths path node net))
net))))))
(defun new-paths (path node net)
(mapcar #'(lambda (n)
(cons n path))
(cdr (assoc node net))))
图 3.12 广度优先搜索(breadth-first search) ![]() 图 3.13 最小网络 要找到从节点 > (cdr (assoc 'a min))
(B C)
图 3.12 程序使用广度优先的方式搜索网络。要使用广度优先搜索,你需要维护一个含有未探索节点的队列。每一次你到达一个节点,检查这个节点是否是你要的。如果不是,你把这个节点的子节点加入队列的尾端,并从队列起始选一个节点,从这继续搜索。借由总是把较深的节点放在队列尾端,我们确保网络一次被搜索一层。 图 3.12 中的代码较不复杂地表示这个概念。我们不仅想要找到节点,还想保有我们怎么到那的纪录。所以与其维护一个具有节点的队列 (queue),我们维护一个已知路径的队列,每个已知路径都是一列节点。当我们从队列取出一个元素继续搜索时,它是一个含有队列前端节点的列表,而不只是一个节点而已。 函数
因为 > (shortest-path 'a 'd min)
(A C D)
这是队列在我们连续调用 ((A))
((B A) (C A))
((C A) (C B A))
((C B A) (D C A))
((D C A) (D C B A))
在队列中的第二个元素变成下一个队列的第一个元素。队列的第一个元素变成下一个队列尾端元素的 在图 3.12 的代码不是搜索一个网络最快的方法,但它给出了列表具有多功能的概念。在这个简单的程序中,我们用三种不同的方式使用了列表:我们使用一个符号的列表来表示路径,一个路径的列表来表示在广度优先搜索中的队列 [4] ,以及一个关联列表来表示网络本身。 3.16 垃圾 (Garbages)¶有很多原因可以使列表变慢。列表提供了顺序存取而不是随机存取,所以列表取出一个指定的元素比数组慢,同样的原因,录音带取出某些东西比在光盘上慢。电脑内部里, Cons 对象倾向于用指针表示,所以走访一个列表意味着走访一系列的指针,而不是简单地像数组一样增加索引值。但这两个所花的代价与配置及回收 Cons 核 (cons cells)比起来小多了。 自动内存管理(Automatic memory management)是 Lisp 最有价值的特色之一。 Lisp 系统维护着一段內存称之为堆(Heap)。系统持续追踪堆当中没有使用的内存,把这些内存发放给新产生的对象。举例来说,函数 如果内存永远没有释放, Lisp 会因为创建新对象把内存用完,而必须要关闭。所以系统必须周期性地通过搜索堆 (heap),寻找不需要再使用的内存。不需要再使用的内存称之为垃圾 (garbage),而清除垃圾的动作称为垃圾回收 (garbage collection或 GC)。 垃圾是从哪来的?让我们来创造一些垃圾: > (setf lst (list 'a 'b 'c))
(A B C)
> (setf lst nil)
NIL
一开始我们调用 因为我们没有任何方法再存取列表,它也有可能是不存在的。我们不再有任何方式可以存取的对象叫做垃圾。系统可以安全地重新使用这三个 Cons 核。 这种管理內存的方法,给程序員带来极大的便利性。你不用显式地配置 (allocate)或释放 (dellocate)內存。这也表示了你不需要处理因为这么做而可能产生的臭虫。內存泄漏 (Memory leaks)以及迷途指针 (dangling pointer)在 Lisp 中根本不可能发生。 但是像任何的科技进步,如果你不小心的话,自动內存管理也有可能对你不利。使用及回收堆所带来的代价有时可以看做 除非你很小心,不然很容易写出过度显式创建 cons 对象的程序。举例来说, 当写出 无论如何 consing 在原型跟实验时是好的。而且如果你利用了列表给你带来的灵活性,你有较高的可能写出后期可存活下来的程序。 Chapter 3 总结 (Summary)¶
Chapter 3 习题 (Exercises)¶
(a) (a b (c d))
(b) (a (b (c (d))))
(c) (((a b) c) d)
(d) (a (b . c) d)
> (new-union '(a b c) '(b a d))
(A B C D)
> (occurrences '(a b a d a c d c a))
((A . 4) (C . 2) (D . 2) (B . 1))
> (pos+ '(7 5 1 4))
(7 6 3 7)
使用 (a) 递归 (b) 迭代 (c)
(a) cons
(b) list
(c) length (for lists)
(d) member (for lists; no keywords)
勘误: 要解决 3.6 (b),你需要使用到 6.3 节的参数
> (showdots '(a b c))
(A . (B . (C . NIL)))
NIL
脚注
第四章:特殊数据结构¶在之前的章节里,我们讨论了列表,Lisp 最多功能的数据结构。本章将演示如何使用 Lisp 其它的数据结构:数组(包含向量与字符串),结构以及哈希表。它们或许不像列表这么灵活,但存取速度更快并使用了更少空间。 Common Lisp 还有另一种数据结构:实例(instance)。实例将在 11 章讨论,讲述 CLOS。 4.1 数组 (Array)¶在 Common Lisp 里,你可以调用 > (setf arr (make-array '(2 3) :initial-element nil))
#<Simple-Array T (2 3) BFC4FE>
Common Lisp 的数组至少可以达到七个维度,每个维度至少可以容纳 1023 个元素。
用 > (aref arr 0 0)
NIL
要替换数组的某个元素,我们使用 > (setf (aref arr 0 0) 'b)
B
> (aref arr 0 0)
B
要表示字面常量的数组(literal array),使用 #2a((b nil nil) (nil nil nil))
如果全局变量 > (setf *print-array* t)
T
> arr
#2A((B NIL NIL) (NIL NIL NIL))
如果我们只想要一维的数组,你可以给 > (setf vec (make-array 4 :initial-element nil))
#(NIL NIL NIL NIL)
一维数组又称为向量(vector)。你可以通过调用 > (vector "a" 'b 3)
#("a" b 3)
字面常量的数组可以表示成 可以用 > (svref vec 0)
NIL
在 4.2 示例:二叉搜索 (Example: Binary Search)¶作为一个示例,这小节演示如何写一个在排序好的向量里搜索对象的函数。如果我们知道一个向量是排序好的,我们可以比(65页) 图 4.1 包含了一个这么工作的函数。其实这两个函数: (defun bin-search (obj vec)
(let ((len (length vec)))
(and (not (zerop len))
(finder obj vec 0 (- len 1)))))
(defun finder (obj vec start end)
(let ((range (- end start)))
(if (zerop range)
(if (eql obj (aref vec start))
obj
nil)
(let ((mid (+ start (round (/ range 2)))))
(let ((obj2 (aref vec mid)))
(if (< obj obj2)
(finder obj vec start (- mid 1))
(if (> obj obj2)
(finder obj vec (+ mid 1) end)
obj)))))))
图 4.1: 搜索一个排序好的向量 如果要找的 如果我们插入下面这行至 (format t "~A~%" (subseq vec start (+ end 1)))
我们可以观察被搜索的元素的数量,是每一步往左减半的: > (bin-search 3 #(0 1 2 3 4 5 6 7 8 9))
#(0 1 2 3 4 5 6 7 8 9)
#(0 1 2 3)
#(3)
3
4.3 字符与字符串 (Strings and Characters)¶字符串是字符组成的向量。我们用一系列由双引号包住的字符,来表示一个字符串常量,而字符 每个字符都有一个相关的整数 ── 通常是 ASCII 码,但不一定是。在多数的 Lisp 实现里,函数 字符比较函数 > (sort "elbow" #'char<)
"below"
由于字符串是字符向量,序列与数组的函数都可以用在字符串。你可以用 > (aref "abc" 1)
#\b
但针对字符串可以使用更快的 > (char "abc" 1)
#\b
可以使用 > (let ((str (copy-seq "Merlin")))
(setf (char str 3) #\k)
str)
如果你想要比较两个字符串,你可以使用通用的 > (equal "fred" "fred")
T
> (equal "fred" "Fred")
NIL
>(string-equal "fred" "Fred")
T
Common Lisp 提供大量的操控、比较字符串的函数。收录在附录 D,从 364 页开始。 有许多方式可以创建字符串。最普遍的方式是使用 > (format nil "~A or ~A" "truth" "dare")
"truth or dare"
但若你只想把数个字符串连结起来,你可以使用 > (concatenate 'string "not " "to worry")
"not to worry"
4.4 序列 (Sequences)¶在 Common Lisp 里,序列类型包含了列表与向量(因此也包含了字符串)。有些用在列表的函数,实际上是序列函数,包括 > (mirror? "abba")
T
我们已经看过四种用来取出序列元素的函数: 给列表使用的 > (elt '(a b c) 1)
B
针对特定类型的序列,特定的存取函数会比较快,所以使用 使用 (defun mirror? (s)
(let ((len (length s)))
(and (evenp len)
(do ((forward 0 (+ forward 1))
(back (- len 1) (- back 1)))
((or (> forward back)
(not (eql (elt s forward)
(elt s back))))
(> forward back))))))
这个版本也可用在列表,但这个实现更适合给向量使用。频繁的对列表调用 许多序列函数接受一个或多个,由下表所列的标准关键字参数:
一个接受所有关键字参数的函数是 > (position #\a "fantasia")
1
> (position #\a "fantasia" :start 3 :end 5)
4
第二个例子我们要找在第四个与第六个字符间,第一个 如果我们给入 > (position #\a "fantasia" :from-end t)
7
我们得到最靠近结尾的
> (position 'a '((c d) (a b)) :key #'car)
1
那么我们要找的是,元素的
> (position '(a b) '((a b) (c d)))
NIL
> (position '(a b) '((a b) (c d)) :test #'equal)
0
> (position 3 '(1 0 7 5) :test #'<)
2
使用 (defun second-word (str)
(let ((p1 (+ (position #\ str) 1)))
(subseq str p1 (position #\ str :start p1))))
返回字符串中第一个单字空格后的第二个单字: > (second-word "Form follows function")
"follows"
要找到满足谓词的元素,其中谓词接受一个实参,我们使用 > (position-if #'oddp '(2 3 4 5))
1
有许多相似的函数,如给序列使用的 > (find #\a "cat")
#\a
> (find-if #'characterp "ham")
#\h
不同于 通常一个 (find-if #'(lambda (x)
(eql (car x) 'complete))
lst)
可以更好的解读为 (find 'complete lst :key #'car)
函数 > (remove-duplicates "abracadabra")
"cdbra"
这个函数接受前表所列的所有关键字参数。 函数 (reduce #'fn '(a b c d))
等同于 (fn (fn (fn 'a 'b) 'c) 'd)
我们可以使用 > (reduce #'intersection '((b r a d 's) (b a d) (c a t)))
(A)
4.5 示例:解析日期 (Example: Parsing Dates)¶作为序列操作的示例,本节演示了如何写程序来解析日期。我们将编写一个程序,可以接受像是 “16 Aug 1980” 的字符串,然后返回一个表示日、月、年的整数列表。 (defun tokens (str test start)
(let ((p1 (position-if test str :start start)))
(if p1
(let ((p2 (position-if #'(lambda (c)
(not (funcall test c)))
str :start p1)))
(cons (subseq str p1 p2)
(if p2
(tokens str test p2)
nil)))
nil)))
(defun constituent (c)
(and (graphic-char-p c)
(not (char= c #\ ))))
图 4.2 辨别符号 (token) 图 4.2 里包含了某些在这个应用里所需的通用解析函数。第一个函数 > (tokens "ab12 3cde.f" #'alpha-char-p 0)
("ab" "cde" "f")
所有不满足此函数的字符被视为空白 ── 他们是语元的分隔符,但永远不是语元的一部分。 函数 在 Common Lisp 里,图形字符是我们可见的字符,加上空白字符。所以如果我们用 > (tokens "ab12 3cde.f gh" #'constituent 0)
("ab12" "3cde.f" "gh")
则语元将会由空白区分出来。 图 4.3 包含了特别为解析日期打造的函数。函数 > (parse-date "16 Aug 1980")
(16 8 1980)
(defun parse-date (str)
(let ((toks (tokens str #'constituent 0)))
(list (parse-integer (first toks))
(parse-month (second toks))
(parse-integer (third toks)))))
(defconstant month-names
#("jan" "feb" "mar" "apr" "may" "jun"
"jul" "aug" "sep" "oct" "nov" "dec"))
(defun parse-month (str)
(let ((p (position str month-names
:test #'string-equal)))
(if p
(+ p 1)
nil)))
图 4.3 解析日期的函数
如果需要自己写程序来解析整数,也许可以这么写: (defun read-integer (str)
(if (every #'digit-char-p str)
(let ((accum 0))
(dotimes (pos (length str))
(setf accum (+ (* accum 10)
(digit-char-p (char str pos)))))
accum)
nil))
这个定义演示了在 Common Lisp 中,字符是如何转成数字的 ── 函数 4.6 结构 (Structures)¶结构可以想成是豪华版的向量。假设你要写一个程序来追踪长方体。你可能会想用三个向量元素来表示长方体:高度、宽度及深度。与其使用原本的 (defun block-height (b) (svref b 0))
而结构可以想成是,这些函数通通都替你定义好了的向量。 要想定义结构,使用 (defstruct point
x
y)
这里定义了一个 2.3 节提过, Lisp 程序可以写出 Lisp 程序。这是目前所见的明显例子之一。当你调用 每一个 (setf p (make-point :x 0 :y 0))
#S(POINT X 0 Y 0)
存取 > (point-x p)
0
> (setf (point-y p) 2)
2
> p
#S(POINT X 0 Y 2)
定义结构也定义了以结构为名的类型。每个点的类型层级会是,类型 > (point-p p)
T
> (typep p 'point)
T
我们可以在本来的定义中,附上一个列表,含有字段名及缺省表达式,来指定结构字段的缺省值。 (defstruct polemic
(type (progn
(format t "What kind of polemic was it? ")
(read)))
(effect nil))
如果 > (make-polemic)
What kind of polemic was it? scathing
#S(POLEMIC :TYPE SCATHING :EFFECT NIL)
结构显示的方式也可以控制,以及结构自动产生的存取函数的字首。以下是做了前述两件事的 (defstruct (point (:conc-name p)
(:print-function print-point))
(x 0)
(y 0))
(defun print-point (p stream depth)
(format stream "#<~A, ~A>" (px p) (py p)))
函数 > (make-point)
#<0,0>
4.7 示例:二叉搜索树 (Example: Binary Search Tree)¶由于 ![]() 图 4.4: 二叉搜索树 二叉搜索树是一种二叉树,给定某个排序函数,比如 图 4.5 包含了二叉搜索树的插入与寻找的函数。基本的数据结构会是 (defstruct (node (:print-function
(lambda (n s d)
(format s "#<~A>" (node-elt n)))))
elt (l nil) (r nil))
(defun bst-insert (obj bst <)
(if (null bst)
(make-node :elt obj)
(let ((elt (node-elt bst)))
(if (eql obj elt)
bst
(if (funcall < obj elt)
(make-node
:elt elt
:l (bst-insert obj (node-l bst) <)
:r (node-r bst))
(make-node
:elt elt
:r (bst-insert obj (node-r bst) <)
:l (node-l bst)))))))
(defun bst-find (obj bst <)
(if (null bst)
nil
(let ((elt (node-elt bst)))
(if (eql obj elt)
bst
(if (funcall < obj elt)
(bst-find obj (node-l bst) <)
(bst-find obj (node-r bst) <))))))
(defun bst-min (bst)
(and bst
(or (bst-min (node-l bst)) bst)))
(defun bst-max (bst)
(and bst
(or (bst-max (node-r bst)) bst)))
图 4.5 二叉搜索树:查询与插入 一棵二叉搜索树可以是 > (setf nums nil)
NIL
> (dolist (x '(5 8 4 2 1 9 6 7 3))
(setf nums (bst-insert x nums #'<)))
NIL
图 4.4 显示了此时 我们可以使用 与 > (bst-find 12 nums #'<)
NIL
> (bst-find 4 nums #'<)
#<4>
这使我们可以区分出无法找到某个值,以及成功找到 要找到二叉搜索树的最小及最大的元素是很简单的。要找到最小的,我们沿着左子树的路径走,如同 > (bst-min nums)
#<1>
> (bst-max nums)
#<9>
要从二叉搜索树里移除元素一样很快,但需要更多代码。图 4.6 演示了如何从二叉搜索树里移除元素。 (defun bst-remove (obj bst <)
(if (null bst)
nil
(let ((elt (node-elt bst)))
(if (eql obj elt)
(percolate bst)
(if (funcall < obj elt)
(make-node
:elt elt
:l (bst-remove obj (node-l bst) <)
:r (node-r bst))
(make-node
:elt elt
:r (bst-remove obj (node-r bst) <)
:l (node-l bst)))))))
(defun percolate (bst)
(cond ((null (node-l bst))
(if (null (node-r bst))
nil
(rperc bst)))
((null (node-r bst)) (lperc bst))
(t (if (zerop (random 2))
(lperc bst)
(rperc bst)))))
(defun rperc (bst)
(make-node :elt (node-elt (node-r bst))
:l (node-l bst)
:r (percolate (node-r bst))))
图 4.6 二叉搜索树:移除 勘误: 此版 函数 > (setf nums (bst-remove 2 nums #'<))
#<5>
> (bst-find 2 nums #'<)
NIL
此时 ![]() 图 4.7: 二叉搜索树 移除需要做更多工作,因为从内部节点移除一个对象时,会留下一个空缺,需要由其中一个孩子来填补。这是 为了要保持树的平衡,如果有两个孩子时, (defun bst-traverse (fn bst)
(when bst
(bst-traverse fn (node-l bst))
(funcall fn (node-elt bst))
(bst-traverse fn (node-r bst))))
图 4.8 二叉搜索树:遍历 一旦我们把一个对象集合插入至二叉搜索树时,中序遍历会将它们由小至大排序。这是图 4.8 中, > (bst-traverse #'princ nums)
13456789
NIL
(函数 本节所给出的代码,提供了一个二叉搜索树实现的脚手架。你可能想根据应用需求,来充实这个脚手架。举例来说,这里所给出的代码每个节点只有一个 二叉搜索树不仅是维护一个已排序对象的集合的方法。他们是否是最好的方法,取决于你的应用。一般来说,二叉搜索树最适合用在插入与删除是均匀分布的情况。有一件二叉搜索树不擅长的事,就是用来维护优先队列(priority queues)。在一个优先队列里,插入也许是均匀分布的,但移除总是在一个另一端。这会导致一个二叉搜索树变得不平衡,而我们期望的复杂度是 4.8 哈希表 (Hash Table)¶第三章演示过列表可以用来表示集合(sets)与映射(mappings)。但当列表的长度大幅上升时(或是 10 个元素),使用哈希表的速度比较快。你通过调用 > (setf ht (make-hash-table))
#<Hash-Table BF0A96>
和函数一样,哈希表总是用 一个哈希表,与一个关联列表类似,是一种表达对应关系的方式。要取出与给定键值有关的数值,我们调用 > (gethash 'color ht)
NIL
NIL
在这里我们首次看到 Common Lisp 最突出的特色之一:一个表达式竟然可以返回多个数值。函数 大部分的实现会在顶层显示一个函数调用的所有返回值,但仅期待一个返回值的代码,只会收到第一个返回值。 5.5 节会说明,代码如何接收多个返回值。 要把数值与键值作关联,使用 > (setf (gethash 'color ht) 'red)
RED
现在如果我们再次调用 > (gethash 'color ht)
RED
T
第二个返回值证明,我们取得了一个真正储存的对象,而不是预设值。 存在哈希表的对象或键值可以是任何类型。举例来说,如果我们要保留函数的某种讯息,我们可以使用哈希表,用函数作为键值,字符串作为词条(entry): > (setf bugs (make-hash-table))
#<Hash-Table BF4C36>
> (push "Doesn't take keyword arguments."
(gethash #'our-member bugs))
("Doesn't take keyword arguments.")
由于 可以用哈希表来取代用列表表示集合。当集合变大时,哈希表的查询与移除会来得比较快。要新增一个成员到用哈希表所表示的集合,把 > (setf fruit (make-hash-table))
#<Hash-Table BFDE76>
> (setf (gethash 'apricot fruit) t)
T
然后要测试是否为成员,你只要调用: > (gethash 'apricot fruit)
T
T
由于 要从集合中移除一个对象,你可以调用 > (remhash 'apricot fruit)
T
返回值说明了是否有词条被移除;在这个情况里,有。 哈希表有一个迭代函数: > (setf (gethash 'shape ht) 'spherical
(gethash 'size ht) 'giant)
GIANT
> (maphash #'(lambda (k v)
(format t "~A = ~A~%" k v))
ht)
SHAPE = SPHERICAL
SIZE = GIANT
COLOR = RED
NIL
哈希表可以容纳任何数量的元素,但当哈希表空间用完时,它们会被扩张。如果你想要确保一个哈希表,从特定数量的元素空间大小开始时,可以给
会返回一个预期存放五个元素的哈希表。 和任何牵涉到查询的结构一样,哈希表一定有某种比较键值的概念。预设是使用 > (setf writers (make-hash-table :test #'equal))
#<Hash-Table C005E6>
> (setf (gethash '(ralph waldo emerson) writers) t)
T
这是一个让哈希表变得有效率的取舍之一。有了列表,我们可以指定 大多数 Lisp 编程的取舍(或是生活,就此而论)都有这种特质。起初你想要事情进行得流畅,甚至赔上效率的代价。之后当代码变得沉重时,你牺牲了弹性来换取速度。 Chapter 4 总结 (Summary)¶
Chapter 4 习题 (Exercises)¶
> (quarter-turn #2A((a b) (c d)))
#2A((C A) (D B))
你会需要用到 361 页的
(a) copy-list
(b) reverse(针对列表)
(a) 一个函数来复制这样的树(复制完的节点与本来的节点是不相等( `eql` )的)
(b) 一个函数,接受一个对象与这样的树,如果对象与树中各节点的其中一个字段相等时,返回真。
勘误:
(a) 接受一个关联列表,并返回一个对应的哈希表。
(b) 接受一个哈希表,并返回一个对应的关联列表。
脚注
第五章:控制流¶2.2 节介绍过 Common Lisp 的求值规则,现在你应该很熟悉了。本章的操作符都有一个共同点,就是它们都违反了求值规则。这些操作符让你决定在程序当中何时要求值。如果普通的函数调用是 Lisp 程序的树叶的话,那这些操作符就是连结树叶的树枝。 5.1 区块 (Blocks)¶Common Lisp 有三个构造区块(block)的基本操作符: > (progn
(format t "a")
(format t "b")
(+ 11 12))
ab
23
由于只返回最后一个表达式的值,代表著使用 一个 > (block head
(format t "Here we go.")
(return-from head 'idea)
(format t "We'll never see this."))
Here we go.
IDEA
调用 也有一个 > (block nil
(return 27))
27
许多接受一个表达式主体的 Common Lisp 操作符,皆隐含在一个叫做 > (dolist (x '(a b c d e))
(format t "~A " x)
(if (eql x 'c)
(return 'done)))
A B C
DONE
使用 (defun foo ()
(return-from foo 27))
在一个显式或隐式的 使用 (defun read-integer (str)
(let ((accum 0))
(dotimes (pos (length str))
(let ((i (digit-char-p (char str pos))))
(if i
(setf accum (+ (* accum 10) i))
(return-from read-integer nil))))
accum))
68 页的版本在构造整数之前,需检查所有的字符。现在两个步骤可以结合,因为如果遇到非数字的字符时,我们可以舍弃计算结果。出现在主体的原子(atom)被解读为标签(labels);把这样的标签传给 > (tagbody
(setf x 0)
top
(setf x (+ x 1))
(format t "~A " x)
(if (< x 10) (go top)))
1 2 3 4 5 6 7 8 9 10
NIL
这个操作符主要用来实现其它的操作符,不是一般会用到的操作符。大多数迭代操作符都隐含在一个 如何决定要使用哪一种区块建构子呢(block construct)?几乎任何时候,你会使用 5.2 语境 (Context)¶另一个我们用来区分表达式的操作符是 > (let ((x 7)
(y 2))
(format t "Number")
(+ x y))
Number
9
一个像是 概念上说,一个 > ((lambda (x) (+ x 1)) 3)
4
前述的 ((lambda (x y)
(format t "Number")
(+ x y))
7
2)
如果有关于 这个模型清楚的告诉我们,由 (let ((x 2)
(y (+ x 1)))
(+ x y))
在 ((lambda (x y) (+ x y)) 2
(+ x 1))
这里明显看到 所以如果你真的想要新变量的值,依赖同一个表达式所设立的另一个变量?在这个情况下,使用一个变形版本 > (let* ((x 1)
(y (+ x 1)))
(+ x y))
3
一个 (let ((x 1))
(let ((y (+ x 1)))
(+ x y)))
> (let (x y)
(list x y))
(NIL NIL)
> (destructuring-bind (w (x y) . z) '(a (b c) d e)
(list w x y z))
(A B C (D E))
若给定的树(第二个实参)没有与模式匹配(第一个参数)时,会产生错误。 5.3 条件 (Conditionals)¶最简单的条件式是 (when (oddp that)
(format t "Hmm, that's odd.")
(+ that 1))
等同于 (if (oddp that)
(progn
(format t "Hmm, that's odd.")
(+ that 1)))
所有条件式的母体 (从正反两面看) 是 (defun our-member (obj lst)
(if (atom lst)
nil
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst)))))
也可以定义成: (defun our-member (obj lst)
(cond ((atom lst) nil)
((eql (car lst) obj) lst)
(t (our-member obj (cdr lst)))))
事实上,Common Lisp 实现大概会把 总得来说呢, > (cond (99))
99
则会返回条件式的值。 由于 当你想要把一个数值与一系列的常量比较时,有 (defun month-length (mon)
(case mon
((jan mar may jul aug oct dec) 31)
((apr jun sept nov) 30)
(feb (if (leap-year) 29 28))
(otherwise "unknown month")))
一个 缺省子句的键值可以是 > (case 99 (99))
NIL
则
5.4 迭代 (Iteration)¶最基本的迭代操作符是 2.13 节提到 (variable initial update)
在 23 页的例子中(译注: 2.13 节), (defun show-squares (start end)
(do ((i start (+ i 1)))
((> i end) 'done)
(format t "~A ~A~%" i (* i i))))
当同时更新超过一个变量时,问题来了,如果一个 > (let ((x 'a))
(do ((x 1 (+ x 1))
(y x x))
((> x 5))
(format t "(~A ~A) " x y)))
(1 A) (2 1) (3 2) (4 3) (5 4)
NIL
每一次迭代时, 但也有一个 > (do* ((x 1 (+ x 1))
(y x x))
((> x 5))
(format t "(~A ~A) " x y))
(1 1) (2 2) (3 3) (4 4) (5 5)
NIL
除了 > (dolist (x '(a b c d) 'done)
(format t "~A " x))
A B C D
DONE
当迭代结束时,初始列表内的第三个表达式 (译注: 有着同样的精神的是 (dotimes (x 5 x)
(format t "~A " x))
0 1 2 3 4
5
(译注:第三个表达式即上例之 Note do 的重点 (THE POINT OF do) 在 “The Evolution of Lisp” 里,Steele 与 Garbriel 陈述了 do 的重点, 表达的实在太好了,值得整个在这里引用过来: 撇开争论语法不谈,有件事要说明的是,在任何一个编程语言中,一个循环若一次只能更新一个变量是毫无用处的。 几乎在任何情况下,会有一个变量用来产生下个值,而另一个变量用来累积结果。如果循环语法只能产生变量, 那么累积结果就得借由赋值语句来“手动”实现…或有其他的副作用。具有多变量的 do 循环,体现了产生与累积的本质对称性,允许可以无副作用地表达迭代过程: (defun factorial (n)
(do ((j n (- j 1))
(f 1 (* j f)))
((= j 0) f)))
当然在 step 形式里实现所有的实际工作,一个没有主体的 do 循环形式是较不寻常的。 函数 > (mapc #'(lambda (x y)
(format t "~A ~A " x y))
'(hip flip slip)
'(hop flop slop))
HIP HOP FLIP FLOP SLIP SLOP
(HIP FLIP SLIP)
总是返回 5.5 多值 (Multiple Values)¶曾有人这么说,为了要强调函数式编程的重要性,每个 Lisp 表达式都返回一个值。现在事情不是这么简单了;在 Common Lisp 里,一个表达式可以返回零个或多个数值。最多可以返回几个值取决于各家实现,但至少可以返回 19 个值。 多值允许一个函数返回多件事情的计算结果,而不用构造一个特定的结构。举例来说,内置的 多值也使得查询函数可以分辨出
> (values 'a nil (+ 2 4))
A
NIL
6
如果一个 > ((lambda () ((lambda () (values 1 2)))))
1
2
然而若只预期一个返回值时,第一个之外的值会被舍弃: > (let ((x (values 1 2)))
x)
1
通过不带实参使用 > (values)
> (let ((x (values)))
x)
NIL
要接收多个数值,我们使用 > (multiple-value-bind (x y z) (values 1 2 3)
(list x y z))
(1 2 3)
> (multiple-value-bind (x y z) (values 1 2)
(list x y z))
(1 2 NIL)
如果变量的数量大于数值的数量,剩余的变量会是 > (multiple-value-bind (s m h) (get-decoded-time)
(format t "~A:~A:~A" h m s))
"4:32:13"
你可以借由 > (multiple-value-call #'+ (values 1 2 3))
6
还有一个函数是 > (multiple-value-list (values 'a 'b 'c))
(A B C)
看起来像是使用 5.6 中止 (Aborts)¶你可以使用 (defun super ()
(catch 'abort
(sub)
(format t "We'll never see this.")))
(defun sub ()
(throw 'abort 99))
表达式依序求值,就像它们是在 > (super)
99
一个带有给定标签的 调用 > (progn
(error "Oops!")
(format t "After the error."))
Error: Oops!
Options: :abort, :backtrace
>>
译注:2 个 关于错误与状态的更多讯息,参见 14.6 小节以及附录 A。 有时候你想要防止代码被 > (setf x 1)
1
> (catch 'abort
(unwind-protect
(throw 'abort 99)
(setf x 2)))
99
> x
2
在这里,即便 5.7 示例:日期运算 (Example: Date Arithmetic)¶在某些应用里,能够做日期的加减是很有用的 ── 举例来说,能够算出从 1997 年 12 月 17 日,六十天之后是 1998 年 2 月 15 日。在这个小节里,我们会编写一个实用的工具来做日期运算。我们会将日期转成整数,起始点设置在 2000 年 1 月 1 日。我们会使用内置的 要将日期转成数字,我们需要从日期的单位中,算出总天数有多少。举例来说,2004 年 11 月 13 日的天数总和,是从起始点至 2004 年有多少天,加上从 2004 年到 2004 年 11 月有多少天,再加上 13 天。 有一个我们会需要的东西是,一张列出非润年每月份有多少天的表格。我们可以使用 Lisp 来推敲出这个表格的内容。我们从列出每月份的长度开始: > (setf mon '(31 28 31 30 31 30 31 31 30 31 30 31))
(31 28 31 30 31 30 31 31 30 31 30 31)
我们可以通过应用 > (apply #'+ mon)
365
现在如果我们反转这个列表并使用 > (setf nom (reverse mon))
(31 30 31 30 31 31 30 31 30 31 28 31)
> (setf sums (maplist #'(lambda (x)
(apply #'+ x))
nom))
(365 334 304 273 243 212 181 151 120 90 59 31)
这些数字体现了从二月一号开始已经过了 31 天,从三月一号开始已经过了 59 天……等等。 我们刚刚建立的这个列表,可以转换成一个向量,见图 5.1,转换日期至整数的代码。 (defconstant month
#(0 31 59 90 120 151 181 212 243 273 304 334 365))
(defconstant yzero 2000)
(defun leap? (y)
(and (zerop (mod y 4))
(or (zerop (mod y 400))
(not (zerop (mod y 100))))))
(defun date->num (d m y)
(+ (- d 1) (month-num m y) (year-num y)))
(defun month-num (m y)
(+ (svref month (- m 1))
(if (and (> m 2) (leap? y)) 1 0)))
(defun year-num (y)
(let ((d 0))
(if (>= y yzero)
(dotimes (i (- y yzero) d)
(incf d (year-days (+ yzero i))))
(dotimes (i (- yzero y) (- d))
(incf d (year-days (+ y i)))))))
(defun year-days (y) (if (leap? y) 366 365))
图 5.1 日期运算:转换日期至数字 典型 Lisp 程序的生命周期有四个阶段:先写好,然后读入,接着编译,最后执行。有件 Lisp 非常独特的事情之一是,在这四个阶段时, Lisp 一直都在那里。可以在你的程序编译 (参见 10.2 小节)或读入时 (参见 14.3 小节) 来调用 Lisp。我们推导出 效率通常只跟第四个阶段有关系,运行期(run-time)。在前三个阶段,你可以随意的使用列表拥有的威力与灵活性,而不需要担心效率。 若你使用图 5.1 的代码来造一个时光机器(time machine),当你抵达时,人们大概会不同意你的日期。即使是相对近的现在,欧洲的日期也曾有过偏移,因为人们会获得更精准的每年有多长的概念。在说英语的国家,最后一次的不连续性出现在 1752 年,日期从 9 月 2 日跳到 9 月 14 日。 每年有几天取决于该年是否是润年。如果该年可以被四整除,我们说该年是润年,除非该年可以被 100 整除,则该年非润年 ── 而要是它可以被 400 整除,则又是润年。所以 1904 年是润年,1900 年不是,而 1600 年是。 要决定某个数是否可以被另个数整除,我们使用函数 > (mod 23 5)
3
> (mod 25 5)
0
如果第一个实参除以第二个实参的余数为 0,则第一个实参是可以被第二个实参整除的。函数 > (mapcar #'leap? '(1904 1900 1600))
(T NIL T)
我们用来转换日期至整数的函数是 要找到从某年开始的天数和, (defun num->date (n)
(multiple-value-bind (y left) (num-year n)
(multiple-value-bind (m d) (num-month left y)
(values d m y))))
(defun num-year (n)
(if (< n 0)
(do* ((y (- yzero 1) (- y 1))
(d (- (year-days y)) (- d (year-days y))))
((<= d n) (values y (- n d))))
(do* ((y yzero (+ y 1))
(prev 0 d)
(d (year-days y) (+ d (year-days y))))
((> d n) (values y (- n prev))))))
(defun num-month (n y)
(if (leap? y)
(cond ((= n 59) (values 2 29))
((> n 59) (nmon (- n 1)))
(t (nmon n)))
(nmon n)))
(defun nmon (n)
(let ((m (position n month :test #'<)))
(values m (+ 1 (- n (svref month (- m 1)))))))
(defun date+ (d m y n)
(num->date (+ (date->num d m y) n)))
图 5.2 日期运算:转换数字至日期 图 5.2 展示了代码的下半部份。函数 和 函数 图 5.2 的前两个函数可以合而为一。与其返回数值给另一个函数, 有了 > (multiple-value-list (date+ 17 12 1997 60))
(15 2 1998)
我们得到,1998 年 2 月 15 日。 Chapter 5 总结 (Summary)¶
Chapter 5 练习 (Exercises)¶
(a) (let ((x (car y)))
(cons x x))
(b) (let* ((w (car x))
(y (+ w z)))
(cons w y))
> (precedes #\a "abracadabra")
(#\c #\d #\r)
> (intersperse '- '(a b c d))
(A - B - C - D)
(a) 递归
(b) do
(c) mapc 与 return
(a) 使用 catch 与 throw 来变更程序,使其找到第一个完整路径时,直接返回它。
(b) 重写一个做到同样事情的程序,但不使用 catch 与 throw。
第六章:函数¶理解函数是理解 Lisp 的关键之一。概念上来说,函数是 Lisp 的核心所在。实际上呢,函数是你手边最有用的工具之一。 6.1 全局函数 (Global Functions)¶谓词 > (fboundp '+)
T
> (symbol-function '+)
#<Compiled-function + 17BA4E>
可通过 (setf (symbol-function 'add2)
#'(lambda (x) (+ x 2)))
新的全局函数可以这样定义,用起来和 > (add2 1)
3
实际上 (defun add2 (x) (+ x 2))
翻译成上述的 通过把 (defun primo (lst) (car lst))
(defun (setf primo) (val lst)
(setf (car lst) val))
在函数名是这种形式 现在任何 > (let ((x (list 'a 'b 'c)))
(setf (primo x) 480)
x)
(480 b c)
不需要为了定义 由于字符串是 Lisp 表达式,没有理由它们不能出现在代码的主体。字符串本身是没有副作用的,除非它是最后一个表达式,否则不会造成任何差别。如果让字符串成为 (defun foo (x)
"Implements an enhanced paradigm of diversity"
x)
那么这个字符串会变成函数的文档字符串(documentation string)。要取得函数的文档字符串,可以通过调用 > (documentation 'foo 'function)
"Implements an enhanced paradigm of diversity"
6.2 局部函数 (Local Functions)¶通过 局部函数可以使用 (name parameters . body)
而 > (labels ((add10 (x) (+ x 10))
(consa (x) (cons 'a x)))
(consa (add10 3)))
(A . 13)
> (labels ((len (lst)
(if (null lst)
0
(+ (len (cdr lst)) 1))))
(len '(a b c)))
3
5.2 节展示了 (do ((x a (b x))
(y c (d y)))
((test x y) (z x y))
(f x y))
等同于 (labels ((rec (x y)
(cond ((test x y)
(z x y))
(t
(f x y)
(rec (b x) (d y))))))
(rec a c))
这个模型可以用来解决,任何你对于 6.3 参数列表 (Parameter Lists)¶2.1 节我们演示过,有了前序表达式, 如果我们在函数的形参列表里的最后一个变量前,插入 (defun our-funcall (fn &rest args)
(apply fn args))
我们也看过操作符中,有的参数可以被忽略,并可以缺省设成特定的值。这样的参数称为选择性参数(optional parameters)。(相比之下,普通的参数有时称为必要参数「required parameters」) 如果符号 (defun philosoph (thing &optional property)
(list thing 'is property))
那么在 > (philosoph 'death)
(DEATH IS NIL)
我们可以明确指定缺省值,通过将缺省值附在列表里给入。这版的 (defun philosoph (thing &optional (property 'fun))
(list thing 'is property))
有着更鼓舞人心的缺省值: > (philosoph 'death)
(DEATH IS FUN)
选择性参数的缺省值可以不是常量。可以是任何的 Lisp 表达式。若这个表达式不是常量,它会在每次需要用到缺省值时被重新求值。 一个关键字参数(keyword parameter)是一种更灵活的选择性参数。如果你把符号 > (defun keylist (a &key x y z)
(list a x y z))
KEYLIST
> (keylist 1 :y 2)
(1 NIL 2 NIL)
> (keylist 1 :y 3 :x 2)
(1 2 3 NIL)
和普通的选择性参数一样,关键字参数缺省值为 关键字与其相关的参数可以被剩余参数收集起来,并传递给其他期望收到这些参数的函数。举例来说,我们可以这样定义 (defun our-adjoin (obj lst &rest args)
(if (apply #'member obj lst args)
lst
(cons obj lst)))
由于 5.2 节介绍过 (destructuring-bind ((&key w x) &rest y) '((:w 3) a)
(list w x y))
(3 NIL (A))
6.4 示例:实用函数 (Example: Utilities)¶2.6 节提到过,Lisp 大部分是由 Lisp 函数组成,这些函数与你可以自己定义的函数一样。这是程序语言中一个有用的特色:你不需要改变你的想法来配合语言,因为你可以改变语言来配合你的想法。如果你想要 Common Lisp 有某个特定的函数,自己写一个,而这个函数会成为语言的一部分,就跟内置的 有经验的 Lisp 程序员,由上而下(top-down)也由下而上 (bottom-up)地工作。当他们朝着语言撰写程序的同时,也打造了一个更适合他们程序的语言。通过这种方式,语言与程序结合的更好,也更好用。 写来扩展 Lisp 的操作符称为实用函数(utilities)。当你写了更多 Lisp 程序时,会发现你开发了一系列的程序,而在一个项目写过许多的实用函数,下个项目里也会派上用场。 专业的程序员常发现,手边正在写的程序,与过去所写的程序有很大的关联。这就是软件重用让人听起来很吸引人的原因。但重用已经被联想成面向对象程序设计。但软件不需要是面向对象的才能重用 ── 这是很明显的,我们看看程序语言(换言之,编译器),是重用性最高的软件。 要获得可重用软件的方法是,由下而上地写程序,而程序不需要是面向对象的才能够由下而上地写出。实际上,函数式风格相比之下,更适合写出重用软件。想想看 (defun single? (lst)
(and (consp lst) (null (cdr lst))))
(defun append1 (lst obj)
(append lst (list obj)))
(defun map-int (fn n)
(let ((acc nil))
(dotimes (i n)
(push (funcall fn i) acc))
(nreverse acc)))
(defun filter (fn lst)
(let ((acc nil))
(dolist (x lst)
(let ((val (funcall fn x)))
(if val (push val acc))))
(nreverse acc)))
(defun most (fn lst)
(if (null lst)
(values nil nil)
(let* ((wins (car lst))
(max (funcall fn wins)))
(dolist (obj (cdr lst))
(let ((score (funcall fn obj)))
(when (> score max)
(setf wins obj
max score))))
(values wins max))))
图 6.1 实用函数 你可以通过撰写实用函数,在程序里做到同样的事情。图 6.1 挑选了一组实用的函数。前两个 > (single? '(a))
T
而后一个函数 > (append1 '(a b c) 'd)
(A B C D)
下个实用函数是 这在测试的时候非常好用(一个 Lisp 的优点之一是,互动环境让你可以轻松地写出测试)。如果我们只想要一个 > (map-int #'identity 10)
(0 1 2 3 4 5 6 7 8 9)
然而要是我们想要一个具有 10 个随机数的列表,每个数介于 0 至 99 之间(包含 99),我们可以忽略参数并只要: > (map-int #'(lambda (x) (random 100))
10)
(85 50 73 64 28 21 40 67 5 32)
我们在 > (filter #'(lambda (x)
(and (evenp x) (+ x 10)))
'(1 2 3 4 5 6 7))
(12 14 16)
另一种思考 图 6.1 的最后一个函数, > (most #'length '((a b) (a b c) (a)))
(A B C)
3
如果平手的话,返回先驰得点的元素。 注意图 6.1 的最后三个函数,它们全接受函数作为参数。 Lisp 使得将函数作为参数传递变得便捷,而这也是为什么,Lisp 适合由下而上程序设计的原因之一。成功的实用函数必须是通用的,当你可以将细节作为函数参数传递时,要将通用的部份抽象起来就变得容易许多。 本节给出的函数是通用的实用函数。可以用在任何种类的程序。但也可以替特定种类的程序撰写实用函数。确实,当我们谈到宏时,你可以凌驾于 Lisp 之上,写出自己的特定语言,如果你想这么做的话。如果你想要写可重用软件,看起来这是最靠谱的方式。 6.5 闭包 (Closures)¶函数可以如表达式的值,或是其它对象那样被返回。以下是接受一个实参,并依其类型返回特定的结合函数: (defun combiner (x)
(typecase x
(number #'+)
(list #'append)
(t #'list)))
在这之上,我们可以创建一个通用的结合函数: (defun combine (&rest args)
(apply (combiner (car args))
args))
它接受任何类型的参数,并以适合它们类型的方式结合。(为了简化这个例子,我们假定所有的实参,都有着一样的类型。) > (combine 2 3)
5
> (combine '(a b) '(c d))
(A B C D)
2.10 小节提过词法变量(lexical variables)只在被定义的上下文内有效。伴随这个限制而来的是,只要那个上下文还有在使用,它们就保证会是有效的。 如果函数在词法变量的作用域里被定义时,函数仍可引用到那个变量,即便函数被作为一个值返回了,返回至词法变量被创建的上下文之外。下面我们创建了一个把实参加上 > (setf fn (let ((i 3))
#'(lambda (x) (+ x i))))
#<Interpreted-Function C0A51E>
> (funcall fn 2)
5
当函数引用到外部定义的变量时,这外部定义的变量称为自由变量(free variable)。函数引用到自由的词法变量时,称之为闭包(closure)。 [2] 只要函数还存在,变量就必须一起存在。 闭包结合了函数与环境(environment);无论何时,当一个函数引用到周围词法环境的某个东西时,闭包就被隐式地创建出来了。这悄悄地发生在像是下面这个函数,是一样的概念: (defun add-to-list (num lst)
(mapcar #'(lambda (x)
(+ x num))
lst))
这函数接受一个数字及列表,并返回一个列表,列表元素是元素与传入数字的和。在 lambda 表达式里的变量 一个更显着的例子会是函数在被调用时,每次都返回不同的闭包。下面这个函数返回一个加法器(adder): (defun make-adder (n)
#'(lambda (x)
(+ x n)))
它接受一个数字,并返回一个将该数字与其参数相加的闭包(函数)。 > (setf add3 (make-adder 3))
#<Interpreted-Function COEBF6>
> (funcall add3 2)
5
> (setf add27 (make-adder 27))
#<Interpreted-Function C0EE4E>
> (funcall add27 2)
29
我们可以产生共享变量的数个闭包。下面我们定义共享一个计数器的两个函数: (let ((counter 0))
(defun reset ()
(setf counter 0))
(defun stamp ()
(setf counter (+ counter 1))))
这样的一对函数或许可以用来创建时间戳章(time-stamps)。每次我们调用 > (list (stamp) (stamp) (reset) (stamp))
(1 2 0 1)
你可以使用全局计数器来做到同样的事情,但这样子使用计数器,可以保护计数器被非预期的引用。 Common Lisp 有一个内置的函数 > (mapcar (complement #'oddp)
'(1 2 3 4 5 6))
(NIL T NIL T NIL T)
有了闭包以后,很容易就可以写出这样的函数: (defun our-complement (f)
#'(lambda (&rest args)
(not (apply f args))))
如果你停下来好好想想,会发现这是个非凡的小例子;而这仅是冰山一角。闭包是 Lisp 特有的美妙事物之一。闭包开创了一种在别的语言当中,像是不可思议的程序设计方法。 6.6 示例:函数构造器 (Example: Function Builders)¶Dylan 是 Common Lisp 与 Scheme 的混合物,有着 Pascal 一般的语法。它有着大量返回函数的函数:除了上一节我们所看过的 complement ,Dylan 包含: (defun compose (&rest fns)
(destructuring-bind (fn1 . rest) (reverse fns)
#'(lambda (&rest args)
(reduce #'(lambda (v f) (funcall f v))
rest
:initial-value (apply fn1 args)))))
(defun disjoin (fn &rest fns)
(if (null fns)
fn
(let ((disj (apply #'disjoin fns)))
#'(lambda (&rest args)
(or (apply fn args) (apply disj args))))))
(defun conjoin (fn &rest fns)
(if (null fns)
fn
(let ((conj (apply #'conjoin fns)))
#'(lambda (&rest args)
(and (apply fn args) (apply conj args))))))
(defun curry (fn &rest args)
#'(lambda (&rest args2)
(apply fn (append args args2))))
(defun rcurry (fn &rest args)
#'(lambda (&rest args2)
(apply fn (append args2 args))))
(defun always (x) #'(lambda (&rest args) x))
图 6.2 Dylan 函数建构器 首先, (compose #'a #'b #'c)
返回一个函数等同于 #'(lambda (&rest args) (a (b (apply #'c args))))
这代表着 下面我们建构了一个函数,先给取参数的平方根,取整后再放回列表里,接著返回: > (mapcar (compose #'list #'round #'sqrt)
'(4 9 16 25))
((2) (3) (4) (5))
接下来的两个函数, > (mapcar (disjoin #'integerp #'symbolp)
'(a "a" 2 3))
(T NIL T T)
> (mapcar (conjoin #'integerp #'symbolp)
'(a "a" 2 3))
(NIL NIL NIL T)
若考虑将谓词定义成集合, cddr = (compose #'cdr #'cdr)
nth = (compose #'car #'nthcdr)
atom = (compose #'not #'consp)
= (rcurry #'typep 'atom)
<= = (disjoin #'< #'=)
listp = (disjoin #'< #'=)
= (rcurry #'typep 'list)
1+ = (curry #'+ 1)
= (rcurry #'+ 1)
1- = (rcurry #'- 1)
mapcan = (compose (curry #'apply #'nconc) #'mapcar
complement = (curry #'compose #'not)
图 6.3 某些等价函数 函数 (curry #'+ 3)
(rcurry #'+ 3)
当函数的参数顺序重要时,很明显可以看出 (funcall (curry #'- 3) 2)
1
而当我们 (funcall (rcurry #'- 3) 2)
-1
最后, 6.7 动态作用域 (Dynamic Scope)¶2.11 小节解释过局部与全局变量的差别。实际的差别是词法作用域(lexical scope)的词法变量(lexical variable),与动态作用域(dynamic scope)的特别变量(special variable)的区别。但这俩几乎是没有区别,因为局部变量几乎总是是词法变量,而全局变量总是是特别变量。 在词法作用域下,一个符号引用到上下文中符号名字出现的地方。局部变量缺省有着词法作用域。所以如果我们在一个环境里定义一个函数,其中有一个变量叫做 (let ((x 10))
(defun foo ()
x))
则无论 > (let ((x 20)) (foo))
10
而动态作用域,我们在环境中函数被调用的地方寻找变量。要使一个变量是动态作用域的,我们需要在任何它出现的上下文中声明它是 (let ((x 10))
(defun foo ()
(declare (special x))
x))
则函数内的 > (let ((x 20))
(declare (special x))
(foo))
20
新的变量被创建出来之后, 一个 通过在顶层调用 > (setf x 30)
30
> (foo)
30
在一个文件里的代码,如果你不想依赖隐式的特殊声明,可以使用 动态作用域什么时候会派上用场呢?通常用来暂时给某个全局变量赋新值。举例来说,有 11 个变量来控制对象印出的方式,包括了 > (let ((*print-base* 16))
(princ 32))
20
32
这里显示了两件事情,由 6.8 编译 (Compilation)¶Common Lisp 函数可以独立被编译或挨个文件编译。如果你只是在顶层输入一个 > (defun foo (x) (+ x 1))
FOO
许多实现会创建一个直译的函数(interpreted function)。你可以将函数传给 > (compiled-function-p #'foo)
NIL
若你将 > (compile 'foo)
FOO
则这个函数会被编译,而直译的定义会被编译出来的取代。编译与直译函数的行为一样,只不过对 你可以把列表作为参数传给 有一种函数你不能作为参数传给 通常要编译 Lisp 代码不是挨个函数编译,而是使用 当一个函数包含在另一个函数内时,包含它的函数会被编译,而且内部的函数也会被编译。所以 > (compile 'make-adder)
MAKE-ADDER
> (compiled-function-p (make-adder 2))
T
6.9 使用递归 (Using Recursion)¶比起多数别的语言,递归在 Lisp 中扮演了一个重要的角色。这主要有三个原因:
学生们起初会觉得递归很难理解。但 3.9 节指出了,如果你想要知道是否正确,不需要去想递归函数所有的调用过程。 同样的如果你想写一个递归函数。如果你可以描述问题是怎么递归解决的,通常很容易将解法转成代码。要使用递归来解决一个问题,你需要做两件事:
如果这两件事完成了,那问题就解决了。因为递归每次都将问题变得更小,而一个有限的问题终究会被解决的,而最小的问题仅需几个有限的步骤就能解决。 举例来说,下面这个找到一个正规列表(proper list)长度的递归算法,我们每次递归时,都可以找到更小列表的长度:
当这个描述翻译成代码时,先处理基本用例;但公式化递归演算法时,我们通常从一般情况下手。 前述的演算法,明确地描述了一种找到正规列表长度的方法。当你定义一个递归函数时,你必须要确定你在分解问题时,问题实际上越变越小。取得一个正规列表的 这里有两个递归算法的示例。假定参数是有限的。注意第二个示例,我们每次递归时,将问题分成两个更小的问题: 第一个例子, 第二个例子, 一旦你可以这样描述算法,要写出递归函数只差一步之遥。 某些算法通常是这样表达最自然,而某些算法不是。你可能需要翻回前面,试试不使用递归来定义 如果你关心效率,有两个你需要考虑的议题。第一,尾递归(tail-recursive),会在 13.2 节讨论。一个好的编译器,使用循环或是尾递归的速度,应该是没有或是区别很小的。然而如果你需要使函数变成尾递归的形式时,或许直接用迭代会更好。 另一个需要铭记在心的议题是,最显而易见的递归算法,不一定是最有效的。经典的例子是费氏函数。它是这样递归地被定义的,
直接翻译这个定义, (defun fib (n)
(if (<= n 1)
1
(+ (fib (- n 1))
(fib (- n 2)))))
这样是效率极差的。一次又一次的重复计算。如果你要找 下面是一个算出同样结果的迭代版本: (defun fib (n)
(do ((i n (- i 1))
(f1 1 (+ f1 f2))
(f2 1 f1))
((<= i 1) f1)))
迭代的版本不如递归版本来得直观,但是效率远远高出许多。这样的事情在实践中常发生吗?非常少 ── 这也是为什么所有的教科书都使用一样的例子 ── 但这是需要注意的事。 Chapter 6 总结 (Summary)¶
Chapter 6 练习 (Exercises)¶
脚注
第七章:输入与输出¶Common Lisp 有着威力强大的 I/O 工具。针对输入以及一些普遍读取字符的函数,我们有 Common Lisp 有两种流 (streams),字符流与二进制流。本章描述了字符流的操作;二进制流的操作涵盖在 14.2 节。 7.1 流 (Streams)¶流是用来表示字符来源或终点的 Lisp 对象。要从文件读取或写入,你将文件作为流打开。但流与文件是不一样的。当你在顶层读入或印出时,你也可以使用流。你甚至可以创建可以读取或写入字符串的流。 输入缺省是从 我们已经看过 路径名(pathname)是一种指定一个文件的可移植方式。路径名包含了六个部分:host、device、directory、name、type 及 version。你可以通过调用 > (setf path (make-pathname :name "myfile"))
#P"myfile"
开启一个文件的基本函数是 你可以在创建流时,指定你想要怎么使用它。 无论你是要写入流、从流读取或者同时进行读写操作,都可以通过 > (setf str (open path :direction :output
:if-exists :supersede))
#<Stream C017E6>
流的打印表示法因实现而异。 现在我们可以把这个流作为第一个参数传给 > (format str "Something~%")
NIL
如果我们在此时检查这个文件,可能有输出,也可能没有。某些实现会将输出累积成一块 (chunks)再输出。直到我们将流关闭,它也许一直不会出现: > (close str)
NIL
当你使用完时,永远记得关闭文件;在你还没关闭之前,内容是不保证会出现的。现在如果我们检查文件 “myfile” ,应该有一行: Something
如果我们只想从一个文件读取,我们可以开启一个具有 > (setf str (open path :direction :input))
#<Stream C01C86>
我们可以对一个文件使用任何输入函数。7.2 节会更详细的描述输入。这里作为一个示例,我们将使用 > (read-line str)
"Something"
> (close str)
NIL
当你读取完毕时,记得关闭文件。 大部分时间我们不使用 (with-open-file (str path :direction :output
:if-exists :supersede)
(format str "Something~%"))
7.2 输入 (Input)¶两个最受欢迎的输入函数是 > (progn
(format t "Please enter your name: ")
(read-line))
Please enter your name: Rodrigo de Bivar
"Rodrigo de Bivar"
NIL
译注:Rodrigo de Bivar 人称熙德 (El Cid),十一世纪的西班牙民族英雄。 如果你想要原封不动的输出,这是你该用的函数。(第二个返回值只在 在一般情况下, 所以要在顶层显示一个文件的内容,我们可以使用下面这个函数: (defun pseudo-cat (file)
(with-open-file (str file :direction :input)
(do ((line (read-line str nil 'eof)
(read-line str nil 'eof)))
((eql line 'eof))
(format t "~A~%" line))))
如果我们想要把输入解析为 Lisp 对象,使用 如果我们在顶层使用 > (read)
(a
b
c)
(A B C)
换句话说,如果我们在一行里面输入许多表达式, > (ask-number)
Please enter a number. a b
Please enter a number. Please enter a number. 43
43
两个连续的提示符 (successive prompts)在第二行被印出。第一个 你或许想要避免使用 > (read-from-string "a b c")
A
2
它同时返回第二个值,一个指出停止读取字符串时的位置的数字。 在一般情况下, 所有的这些输入函数是由基本函数 (primitive) 7.3 输出 (Output)¶三个最简单的输出函数是
> (prin1 "Hello")
"Hello"
"Hello"
> (princ "Hello")
Hello
"Hello"
两者皆返回它们的第一个参数 (译注: 第二个值是返回值) ── 顺道一提,是用 有这些函数的背景知识在解释更为通用的 如果我们把 由于每人的观点不同, > (format nil "Dear ~A, ~% Our records indicate..."
"Mr. Malatesta")
"Dear Mr. Malatesta,
Our records indicate..."
这里
> (format t "~S ~A" "z" "z")
"z" z
NIL
格式化指令可以接受参数。
下面是一个有五个参数的罕见例子: ? (format nil "~10,2,0,'*,' F" 26.21875)
" 26.22"
这是原本的数字取至小数点第二位、(小数点向左移 0 位)、在 10 个字符的空间里向右对齐,左边补满空白。注意作为参数给入是写成 所有的这些参数都是选择性的。要使用缺省值你可以直接忽略对应的参数。如果我们想要做的是,印出一个小数点取至第二位的数字,我们可以说: > (format nil "~,2,,,F" 26.21875)
"26.22"
你也可以忽略一系列的尾随逗号 (trailing commas),前面指令更常见的写法会是: > (format nil "~,2F" 26.21875)
"26.22"
警告: 当 7.4 示例:字符串代换 (Example: String Substitution)¶作为一个 I/O 的示例,本节演示如何写一个简单的程序来对文本文件做字符串替换。我们即将写一个可以将一个文件中,旧的字符串 而要是 一个暂时储存输入的队列 (queue)称作缓冲区 (buffer)。在这个情况里,因为我们知道我们不需要储存超过一个预定的字符量,我们可以使用一个叫做环状缓冲区 在图 7.1 的代码,实现了环状缓冲区的操作。 另外两个索引, start ≤ used ≤ new ≤ end
你可以把 函数 要插入一个新值至缓冲区,我们将使用 (defstruct buf
vec (start -1) (used -1) (new -1) (end -1))
(defun bref (buf n)
(svref (buf-vec buf)
(mod n (length (buf-vec buf)))))
(defun (setf bref) (val buf n)
(setf (svref (buf-vec buf)
(mod n (length (buf-vec buf))))
val))
(defun new-buf (len)
(make-buf :vec (make-array len)))
(defun buf-insert (x b)
(setf (bref b (incf (buf-end b))) x))
(defun buf-pop (b)
(prog1
(bref b (incf (buf-start b)))
(setf (buf-used b) (buf-start b)
(buf-new b) (buf-end b))))
(defun buf-next (b)
(when (< (buf-used b) (buf-new b))
(bref b (incf (buf-used b)))))
(defun buf-reset (b)
(setf (buf-used b) (buf-start b)
(buf-new b) (buf-end b)))
(defun buf-clear (b)
(setf (buf-start b) -1 (buf-used b) -1
(buf-new b) -1 (buf-end b) -1))
(defun buf-flush (b str)
(do ((i (1+ (buf-used b)) (1+ i)))
((> i (buf-end b)))
(princ (bref b i) str)))
图 7.1 环状缓冲区的操作 接下来我们需要两个特别为这个应用所写的函数: 最后 在图 7.1 定义的函数被图 7.2 所使用,包含了字符串替换的代码。函数 第二个函数 变数 (defun file-subst (old new file1 file2)
(with-open-file (in file1 :direction :input)
(with-open-file (out file2 :direction :output
:if-exists :supersede)
(stream-subst old new in out))))
(defun stream-subst (old new in out)
(let* ((pos 0)
(len (length old))
(buf (new-buf len))
(from-buf nil))
(do ((c (read-char in nil :eof)
(or (setf from-buf (buf-next buf))
(read-char in nil :eof))))
((eql c :eof))
(cond ((char= c (char old pos))
(incf pos)
(cond ((= pos len) ; 3
(princ new out)
(setf pos 0)
(buf-clear buf))
((not from-buf) ; 2
(buf-insert c buf))))
((zerop pos) ; 1
(princ c out)
(when from-buf
(buf-pop buf)
(buf-reset buf)))
(t ; 4
(unless from-buf
(buf-insert c buf))
(princ (buf-pop buf) out)
(buf-reset buf)
(setf pos 0))))
(buf-flush buf out)))
图 7.2 字符串替换 下列表格展示了当我们将文件中的
第一栏是当前字符 ── 在文件 The struggle between Liberty and Authority is the most conspicuous feature
in the portions of history with which we are earliest familiar, particularly
in that of Greece, Rome, and England.
在我们对 The struggle between Liberty and Authority is ze most conspicuous feature
in ze portions of history with which we are earliest familiar, particularly
in zat of Greece, Rome, and England.
为了使这个例子尽可能的简单,图 7.2 的代码只将一个字符串换成另一个字符串。很容易扩展为搜索一个模式而不是一个字面字符串。你只需要做的是,将 7.5 宏字符 (Macro Characters)¶一个宏字符 (macro character)是获得 一个宏字符或宏字符组合也称作 > (car (read-from-string "'a"))
QUOTE
引用对于读取宏来说是不寻常的,因为它用单一字符表示。有了一个有限的字符集,你可以在 Common Lisp 里有许多单一字符的读取宏,来表示一个或更多字符。 这样的读取宏叫做派发 (dispatching)读取宏,而第一个字符叫做派发字符 (dispatching character)。所有预定义的派发读取宏使用井号 ( 其它我们见过的派发读取宏包括 > (let ((*print-array* t))
(vectorp (read-from-string (format nil "~S"
(vector 1 2)))))
T
当然我们拿回来的不是同一个向量,而是具有同样元素的新向量。 不是所有对象被显示时都有着清楚 (distinct)、可读的形式。举例来说,函数与哈希表,倾向于这样 当你定义你自己的事物表示法时 (举例来说,结构的印出函数),你要将此准则记住。要不使用一个可以被读回来的表示法,或是使用 Chapter 7 总结 (Summary)¶
Chapter 7 练习 (Exercises)¶
脚注
第八章:符号¶我们一直在使用符号。符号,在看似简单的表面之下,又好像没有那么简单。起初最好不要纠结于背后的实现机制。可以把符号当成数据对象与名字那样使用,而不需要理解两者是如何关联起来的。但到了某个时间点,停下来思考背后是究竟是如何工作会是很有用的。本章解释了背后实现的细节。 8.1 符号名 (Symbol Names)¶第二章描述过,符号是变量的名字,符号本身以对象所存在。但 Lisp 符号的可能性,要比在多数语言仅允许作为变量名来得广泛许多。实际上,符号可以用任何字符串当作名字。可以通过调用 > (symbol-name 'abc)
"ABC"
注意到这个符号的名字,打印出来都是大写字母。缺省情况下, Common Lisp 在读入时,会把符号名字所有的英文字母都转成大写。代表 Common Lisp 缺省是不分大小写的: > (eql 'abc 'Abc)
T
> (CaR '(a b c))
A
一个名字包含空白,或其它可能被读取器认为是重要的字符的符号,要用特殊的语法来引用。任何存在垂直杠 (vertical bar)之间的字符序列将被视为符号。可以如下这般在符号的名字中,放入任何字符: > (list '|Lisp 1.5| '|| '|abc| '|ABC|)
(|Lisp 1.5| || |abc| ABC)
当这种符号被读入时,不会有大小写转换,而宏字符与其他的字符被视为一般字符。 那什么样的符号不需要使用垂直杠来参照呢?基本上任何不是数字,或不包含读取器视为重要的字符的符号。一个快速找出你是否可以不用垂直杠来引用符号的方法,是看看 Lisp 如何印出它的。如果 Lisp 没有用垂直杠表示一个符号,如上述列表的最后一个,那么你也可以不用垂直杠。 记得,垂直杠是一种表示符号的特殊语法。它们不是符号的名字之一: > (symbol-name '|a b c|)
"a b c"
(如果想要在符号名称内使用垂直杠,可以放一个反斜线在垂直杠的前面。) 译注: 反斜线是 8.2 属性列表 (Property Lists)¶在 Common Lisp 里,每个符号都有一个属性列表(property-list)或称为 > (get 'alizarin 'color)
NIL
它使用 要将值与键关联起来时,你可以使用 > (setf (get 'alizarin 'color) 'red)
RED
> (get 'alizarin 'color)
RED
现在符号 ![]() 图 8.1 符号的结构 > (setf (get 'alizarin 'transparency) 'high)
HIGH
> (symbol-plist 'alizarin)
(TRANSPARENCY HIGH COLOR RED)
注意,属性列表不以关联列表(assoc-lists)的形式表示,虽然用起来感觉是一样的。 在 Common Lisp 里,属性列表用得不多。他们大部分被哈希表取代了(4.8 小节)。 8.3 符号很不简单 (Symbols Are Big)¶当我们输入名字时,符号就被悄悄地创建出来了,而当它们被显示时,我们只看的到符号的名字。某些情况下,把符号想成是表面所见的东西就好,别想太多。但有时候符号不像看起来那么简单。 从我们如何使用和检查符号的方式来看,符号像是整数那样的小对象。而符号实际上确实是一个对象,差不多像是由 很少有程序会使用很多符号,以致于值得用其它的东西来代替符号以节省空间。但需要记住的是,符号是实际的对象,不仅是名字而已。当两个变量设成相同的符号时,与两个变量设成相同列表一样:两个变量的指针都指向同样的对象。 8.4 创建符号 (Creating Symbols)¶8.1 节演示了如何取得符号的名字。另一方面,用字符串生成符号也是有可能的。但比较复杂一点,因为我们需要先介绍包(package)。 概念上来说,包是将名字映射到符号的符号表(symbol-tables)。每个普通的符号都属于一个特定的包。符号属于某个包,我们称为符号被包扣押(intern)了。函数与变量用符号作为名称。包借由限制哪个符号可以访问来实现模块化(modularity),也是因为这样,我们才可以引用到函数与变量。 大多数的符号在读取时就被扣押了。在第一次输入一个新符号的名字时,Lisp 会产生一个新的符号对象,并将它扣押到当下的包里(缺省是 > (intern "RANDOM-SYMBOL")
RANDOM-SYMBOL
NIL
选择性包参数缺省是当前的包,所以前述的表达式,返回当前包里的一个符号,此符号的名字是 “RANDOM-SYMBOL”,若此符号尚未存在时,会创建一个这样的符号出来。第二个返回值告诉我们符号是否存在;在这个情况,它不存在。 不是所有的符号都会被扣押。有时候有一个自由的(uninterned)符号是有用的,这和公用电话本是一样的原因。自由的符号叫做 gensyms 。我们将会在第 10 章讨论宏(Macro)时,理解 8.5 多重包 (Multiple Packages)¶大的程序通常切分为多个包。如果程序的每个部分都是一个包,那么开发程序另一个部分的某个人,将可以使用符号来作为函数名或变量名,而不必担心名字在别的地方已经被用过了。 在没有提供定义多个命名空间的语言里,工作于大项目的程序员,通常需要想出某些规范(convention),来确保他们不会使用同样的名称。举例来说,程序员写显示相关的代码(display code)可能用 包不过是提供了一种便捷方式来自动办到此事。如果你将函数定义在单独的包里,可以随意使用你喜欢的名字。只有你明确导出( 举例来说,假设一个程序分为两个包, 下面是你可能会放在文件最上方,包含独立包的代码: (defpackage "MY-APPLICATION"
(:use "COMMON-LISP" "MY-UTILITIES")
(:nicknames "APP")
(:export "WIN" "LOSE" "DRAW"))
(in-package my-application)
8.6 关键字 (Keywords)¶在 为什么使用关键字而不用一般的符号?因为关键字在哪都可以存取。一个函数接受符号作为实参,应该要写成预期关键字的函数。举例来说,这个函数可以安全地在任何包里调用: (defun noise (animal)
(case animal
(:dog :woof)
(:cat :meow)
(:pig :oink)))
但如果是用一般符号写成的话,它只在被定义的包内正常工作,除非关键字也被导出了。 8.7 符号与变量 (Symbols and Variables)¶Lisp 有一件可能会使你困惑的事情是,符号与变量的从两个非常不同的层面互相关联。当符号是特别变量(special variable)的名字时,变量的值存在符号的 value 栏位(图 8.1)。 而对于词法变量(lexical variables)来说,事情就完全不一样了。一个作为词法变量的符号只不过是个占位符(placeholder)。编译器会将其转为一个寄存器(register)或内存位置的引用位址。在最后编译出来的代码中,我们无法追踪这个符号 (除非它被保存在调试器「debugger」的某个地方)。因此符号与词法变量的值之间是没有连接的;只要一有值,符号就消失了。 8.8 示例:随机文本 (Example: Random Text)¶如果你要写一个操作单词的程序,通常使用符号会比字符串来得好,因为符号概念上是原子性的(atomic)。符号可以用 产生的文字将会是部分可信的(locally plausible),因为任两个出现的单词也是输入文件里,两个同时出现的单词。令人惊讶的是,获得看起来是 ── 有意义的整句 ── 甚至整个段落是的频率相当高。 图 8.2 包含了程序的上半部,用来读取示例文件的代码。 (defparameter *words* (make-hash-table :size 10000))
(defconstant maxword 100)
(defun read-text (pathname)
(with-open-file (s pathname :direction :input)
(let ((buffer (make-string maxword))
(pos 0))
(do ((c (read-char s nil :eof)
(read-char s nil :eof)))
((eql c :eof))
(if (or (alpha-char-p c) (char= c #\'))
(progn
(setf (aref buffer pos) c)
(incf pos))
(progn
(unless (zerop pos)
(see (intern (string-downcase
(subseq buffer 0 pos))))
(setf pos 0))
(let ((p (punc c)))
(if p (see p)))))))))
(defun punc (c)
(case c
(#\. '|.|) (#\, '|,|) (#\; '|;|)
(#\! '|!|) (#\? '|?|) ))
(let ((prev `|.|))
(defun see (symb)
(let ((pair (assoc symb (gethash prev *words*))))
(if (null pair)
(push (cons symb 1) (gethash prev *words*))
(incf (cdr pair))))
(setf prev symb)))
图 8.2 读取示例文件 从图 8.2 所导出的数据,会被存在哈希表 ((|sin| . 1) (|wide| . 2) (|sights| . 1))
使用弥尔顿的失乐园作为示例文件时,这是与键 函数 只要下个字符是一个字(由 函数 在 现在来到了有趣的部份。图 8.3 包含了从图 8.2 所累积的数据来产生文字的代码。 (defun generate-text (n &optional (prev '|.|))
(if (zerop n)
(terpri)
(let ((next (random-next prev)))
(format t "~A " next)
(generate-text (1- n) next))))
(defun random-next (prev)
(let* ((choices (gethash prev *words*))
(i (random (reduce #'+ choices
:key #'cdr))))
(dolist (pair choices)
(if (minusp (decf i (cdr pair)))
(return (car pair))))))
图 8.3 产生文字 要取得一个新的单词, 现在会是测试运行下程序的好时机。但其实你早看过一个它所产生的示例: 就是本书开头的那首诗,是使用弥尔顿的失乐园作为输入文件所产生的。 (译注: 诗可在这里看,或是浏览书的第 vi 页) Half lost on my firmness gains more glad heart, Or violent and from forage drives A glimmering of all sun new begun Both harp thy discourse they match’d, Forth my early, is not without delay; For their soft with whirlwind; and balm. Undoubtedly he scornful turn’d round ninefold, Though doubled now what redounds, And chains these a lower world devote, yet inflicted? Till body or rare, and best things else enjoy’d in heav’n To stand divided light at ev’n and poise their eyes, Or nourish, lik’ning spiritual, I have thou appear. ── Henley Chapter 8 总结 (Summary)¶
Chapter 8 练习 (Exercises)¶
脚注
第九章:数字¶处理数字是 Common Lisp 的强项之一。Common Lisp 有着丰富的数值类型,而 Common Lisp 操作数字的特性与其他语言比起来更受人喜爱。 9.1 类型 (Types)¶Common Lisp 提供了四种不同类型的数字:整数、浮点数、比值与复数。本章所讲述的函数适用于所有类型的数字。有几个不能用在复数的函数会特别说明。 整数写成一串数字:如 谓词 ![]() 图 9.1: 数值类型 要决定计算过程会返回何种数字,以下是某些通用的经验法则:
第二、第三个规则可以在读入参数时直接应用,所以: > (list (ratiop 2/2) (complexp #c(1 0)))
(NIL NIL)
9.2 转换及取出 (Conversion and Extraction)¶Lisp 提供四种不同类型的数字的转换及取出位数的函数。函数 > (mapcar #'float '(1 2/3 .5))
(1.0 0.6666667 0.5)
将数字转成整数未必需要转换,因为它可能牵涉到某些资讯的丧失。函数 > (truncate 1.3)
1
0.29999995
第二个返回值 函数 (defun palindrome? (x)
(let ((mid (/ (length x) 2)))
(equal (subseq x 0 (floor mid))
(reverse (subseq x (ceiling mid))))))
和 > (floor 1.5)
1
0.5
实际上,我们可以把 (defun our-truncate (n)
(if (> n 0)
(floor n)
(ceiling n)))
函数 > (mapcar #'round '(-2.5 -1.5 1.5 2.5))
(-2 -2 2 2)
在某些数值应用中这是好事,因为舍入误差(rounding error)通常会互相抵消。但要是用户期望你的程序将某些值取整数时,你必须自己提供这个功能。 [1] 与其他的函数一样, 函数 关于实数,函数 > (mapcar #'signum '(-2 -0.0 0.0 0 .5 3))
(-1 -0.0 0.0 0 1.0 1)
在某些应用里, 比值与复数概念上是两部分的结构。(译注:像 Cons 这样的两部分结构) 函数 函数 9.3 比较 (Comparison)¶谓词 > (= 1 1.0)
T
> (eql 1 1.0)
NIL
用来比较数字的谓词为 (<= w x y z)
等同于二元操作符的结合(conjunction),应用至每一对参数上: (and (<= w x) (<= x y) (<= y z))
由于 (/= w x y z)
等同于 (and (/= w x) (/= w y) (/= w z)
(/= x y) (/= y z) (/= y z))
特殊的谓词 > (list (minusp -0.0) (zerop -0.0))
(NIL T)
因此对 谓词 本节定义的谓词中,只有 函数 > (list (max 1 2 3 4 5) (min 1 2 3 4 5))
(5 1)
如果参数含有浮点数的话,结果的类型取决于各家实现。 9.4 算术 (Arithematic)¶用来做加减的函数是 (- x y z)
等同于 (- (- x y) z)
有两个函数 宏 用来做乘法的函数是 除法函数 > (/ 3)
1/3
而这样形式的调用 (/ x y z)
等同于 (/ (/ x y) z)
注意 当给定两个整数时, > (/ 365 12)
365/12
举例来说,如果你试着找出平均每一个月有多长,可能会有解释器在逗你玩的感觉。在这个情况下,你需要的是,对比值调用 > (float 365/12)
30.416666
9.5 指数 (Exponentiation)¶要找到 \(x^n\) 调用 > (expt 2 5)
32
而要找到 \(log_nx\) 调用 > (log 32 2)
5.0
通常返回一个浮点数。 要找到 \(e^x\) 有一个特别的函数 > (exp 2)
7.389056
而要找到自然对数,你可以使用 > (log 7.389056)
2.0
要找到立方根,你可以调用 > (expt 27 1/3)
3.0
但要找到平方根,函数 > (sqrt 4)
2.0
9.6 三角函数 (Trigometric Functions)¶常量 > (let ((x (/ pi 4)))
(list (sin x) (cos x) (tan x)))
(0.7071067811865475d0 0.7071067811865476d0 1.0d0)
;;; 译注: CCL 1.8 SBCL 1.0.55 下的结果是
;;; (0.7071067811865475D0 0.7071067811865476D0 0.9999999999999999D0)
这些函数都接受负数及复数参数。 函数 双曲正弦、双曲余弦及双曲正交分别由 9.7 表示法 (Representations)¶Common Lisp 没有限制整数的大小。可以塞进一个字(word)内存的小整数称为定长数(fixnums)。在计算过程中,整数无法塞入一个字时,Lisp 切换至使用多个字的表示法(一个大数 「bignum」)。所以整数的大小限制取决于实体内存,而不是语言。 常量 > (values most-positive-fixnum most-negative-fixnum)
536870911
-536870912
;;; 译注: CCL 1.8 的结果为
1152921504606846975
-1152921504606846976
;;; SBCL 1.0.55 的结果为
4611686018427387903
-4611686018427387904
谓词 > (typep 1 'fixnum)
T
> (type (1+ most-positive-fixnum) 'bignum)
T
浮点数的数值限制是取决于各家实现的。 Common Lisp 提供了至多四种类型的浮点数:短浮点 一般来说,短浮点应可塞入一个字,单浮点与双浮点提供普遍的单精度与双精度浮点数的概念,而长浮点,如果想要的话,可以是很大的数。但实现可以不对这四种类型做区别,也是完全没有问题的。 你可以指定你想要何种格式的浮点数,当数字是用科学表示法时,可以通过将 (译注: 在给定的实现里,用十六个全局常量标明了每个格式的限制。它们的名字是这种形式: 浮点数下溢(underflow)与溢出(overflow),都会被 Common Lisp 视为错误 : > (* most-positive-long-float 10)
Error: floating-point-overflow
9.8 范例:追踪光线 (Example: Ray-Tracing)¶作为一个数值应用的范例,本节示范了如何撰写一个光线追踪器 (ray-tracer)。光线追踪是一个高级的 (deluxe)渲染算法: 它产生出逼真的图像,但需要花点时间。 要产生一个 3D 的图像,我们至少需要定义四件事: 一个观测点 (eye)、一个或多个光源、一个由一个或多个平面所组成的模拟世界 (simulated world),以及一个作为通往这个世界的窗户的平面 (图像平面「image plane」)。我们产生出的是模拟世界投影在图像平面区域的图像。 光线追踪独特的地方在于,我们如何找到这个投影: 我们一个一个像素地沿着图像平面走,追踪回到模拟世界里的光线。这个方法带来三个主要的优势: 它让我们容易得到现实世界的光学效应 (optical effect),如透明度 (transparency)、反射光 (reflected light)以及产生阴影 (cast shadows);它让我们可以直接用任何我们想要的几何的物体,来定义出模拟的世界,而不需要用多边形 (polygons)来建构它们;以及它很简单实现。 (defun sq (x) (* x x))
(defun mag (x y z)
(sqrt (+ (sq x) (sq y) (sq z))))
(defun unit-vector (x y z)
(let ((d (mag x y z)))
(values (/ x d) (/ y d) (/ z d))))
(defstruct (point (:conc-name nil))
x y z)
(defun distance (p1 p2)
(mag (- (x p1) (x p2))
(- (y p1) (y p2))
(- (z p1) (z p2))))
(defun minroot (a b c)
(if (zerop a)
(/ (- c) b)
(let ((disc (- (sq b) (* 4 a c))))
(unless (minusp disc)
(let ((discrt (sqrt disc)))
(min (/ (+ (- b) discrt) (* 2 a))
(/ (- (- b) discrt) (* 2 a))))))))
图 9.2 实用数学函数 图 9.2 包含了我们在光线追踪器里会需要用到的一些实用数学函数。第一个 > (multiple-value-call #'mag (unit-vector 23 12 47))
1.0
我们在 最后
\[x = \dfrac{-b \pm \sqrt{b^2 - 4ac}}{2a}\]
图 9.3 包含了定义一个最小光线追踪器的代码。 它产生通过单一光源照射的黑白图像,与观测点 (eye)处于同个位置。 (结果看起来像是闪光摄影术 (flash photography)拍出来的)
(defstruct surface color)
(defparameter *world* nil)
(defconstant eye (make-point :x 0 :y 0 :z 200))
(defun tracer (pathname &optional (res 1))
(with-open-file (p pathname :direction :output)
(format p "P2 ~A ~A 255" (* res 100) (* res 100))
(let ((inc (/ res)))
(do ((y -50 (+ y inc)))
((< (- 50 y) inc))
(do ((x -50 (+ x inc)))
((< (- 50 x) inc))
(print (color-at x y) p))))))
(defun color-at (x y)
(multiple-value-bind (xr yr zr)
(unit-vector (- x (x eye))
(- y (y eye))
(- 0 (z eye)))
(round (* (sendray eye xr yr zr) 255))))
(defun sendray (pt xr yr zr)
(multiple-value-bind (s int) (first-hit pt xr yr zr)
(if s
(* (lambert s int xr yr zr) (surface-color s))
0)))
(defun first-hit (pt xr yr zr)
(let (surface hit dist)
(dolist (s *world*)
(let ((h (intersect s pt xr yr zr)))
(when h
(let ((d (distance h pt)))
(when (or (null dist) (< d dist))
(setf surface s hit h dist d))))))
(values surface hit)))
(defun lambert (s int xr yr zr)
(multiple-value-bind (xn yn zn) (normal s int)
(max 0 (+ (* xr xn) (* yr yn) (* zr zn)))))
图 9.3 光线追踪。 图像平面会是由 x 轴与 y 轴所定义的平面。观测者 (eye) 会在 z 轴,距离原点 200 个单位。所以要在图像平面可以被看到,插入至 ![]() 图 9.4: 追踪光线。 函数 图片的解析度可以通过给入明确的 图片是一个在图像平面 100x100 的正方形。每一个像素代表着穿过图像平面抵达观测点的光的数量。要找到每个像素光的数量, 要决定一个光线的亮度, 朗伯定律 告诉我们,由平面上一点所反射的光的强度,正比于该点的单位法向量 (unit normal vector) N (这里是与平面垂直且长度为一的向量)与该点至光源的单位向量 L 的点积 (dot-product):
\[i = N·L\]
如果光刚好照到这点, N 与 L 会重合 (coincident),则点积会是最大值, 在我们的程序里,我们假设光源在观测点 (eye),所以 在 为了简单起见,我们在模拟世界里会只有一种物体,球体。图 9.5 包含了与球体有关的代码。球体结构包含了 (defstruct (sphere (:include surface))
radius center)
(defun defsphere (x y z r c)
(let ((s (make-sphere
:radius r
:center (make-point :x x :y y :z z)
:color c)))
(push s *world*)
s))
(defun intersect (s pt xr yr zr)
(funcall (typecase s (sphere #'sphere-intersect))
s pt xr yr zr))
(defun sphere-intersect (s pt xr yr zr)
(let* ((c (sphere-center s))
(n (minroot (+ (sq xr) (sq yr) (sq zr))
(* 2 (+ (* (- (x pt) (x c)) xr)
(* (- (y pt) (y c)) yr)
(* (- (z pt) (z c)) zr)))
(+ (sq (- (x pt) (x c)))
(sq (- (y pt) (y c)))
(sq (- (z pt) (z c)))
(- (sq (sphere-radius s)))))))
(if n
(make-point :x (+ (x pt) (* n xr))
:y (+ (y pt) (* n yr))
:z (+ (z pt) (* n zr))))))
(defun normal (s pt)
(funcall (typecase s (sphere #'sphere-normal))
s pt))
(defun sphere-normal (s pt)
(let ((c (sphere-center s)))
(unit-vector (- (x c) (x pt))
(- (y c) (y pt))
(- (z c) (z pt)))))
图 9.5 球体。 函数 我们要怎么找到一束光与一个球体的交点 (intersection)呢?光线是表示成点 \(p =〈x_0,y_0,x_0〉\) 以及单位向量 \(v =〈x_r,y_r,x_r〉\) 。每个在光上的点可以表示为 \(p+nv\) ,对于某个 n ── 即 \(〈x_0+nx_r,y_0+ny_r,z_0+nz_r〉\) 。光击中球体的点的距离至中心 \(〈x_c,y_c,z_c〉\) 会等于球体的半径 r 。所以在下列这个交点的方程序会成立:
\[r = \sqrt{ (x_0 + nx_r - x_c)^2 + (y_0 + ny_r - y_c)^2 + (z_0 + nz_r - z_c)^2 }\]
这会给出
\[an^2 + bn + c = 0\]
其中
\[\begin{split}a = x_r^2 + y_r^2 + z_r^2\\b = 2((x_0-x_c)x_r + (y_0-y_c)y_r + (z_0-z_c)z_r)\\c = (x_0-x_c)^2 + (y_0-y_c)^2 + (z_0-z_c)^2 - r^2\end{split}\]
要找到交点我们只需要找到这个二次方程序的根。它可能是零、一个或两个实数根。没有根代表光没有击中球体;一个根代表光与球体交于一点 (擦过 「grazing hit」);两个根代表光与球体交于两点 (一点交于进入时、一点交于离开时)。在最后一个情况里,我们想要两个根之中较小的那个; n 与光离开观测点的距离成正比,所以先击中的会是较小的 n 。所以我们调用 图 9.5 的另外两个函数, 图 9.6 示范了我们如何产生图片; (译注:PGM 可移植灰度图格式,更多信息参见 wiki ) (defun ray-test (&optional (res 1))
(setf *world* nil)
(defsphere 0 -300 -1200 200 .8)
(defsphere -80 -150 -1200 200 .7)
(defsphere 70 -100 -1200 200 .9)
(do ((x -2 (1+ x)))
((> x 2))
(do ((z 2 (1+ z)))
((> z 7))
(defsphere (* x 200) 300 (* z -400) 40 .75)))
(tracer (make-pathname :name "spheres.pgm") res))
图 9.6 使用光线追踪器 图 9.7 是产生出来的图片,其中 ![]() 图 9.7: 追踪光线的图 一个实际的光线追踪器可以产生更复杂的图片,因为它会考虑更多,我们只考虑了单一光源至平面某一点。可能会有多个光源,每一个有不同的强度。它们通常不会在观测点,在这个情况程序需要检查至光源的向量是否与其他平面相交,这会在第一个相交的平面上产生阴影。将光源放置于观测点让我们不需要考虑这麽复杂的情况,因为我们看不见在阴影中的任何点。 一个实际的光线追踪器不仅追踪光第一个击中的平面,也会加入其它平面的反射光。一个实际的光线追踪器会是有颜色的,并可以模型化出透明或是闪耀的平面。但基本的算法会与图 9.3 所演示的差不多,而许多改进只需要递回的使用同样的成分。 一个实际的光线追踪器可以是高度优化的。这里给出的程序为了精简写成,甚至没有如 Lisp 程序员会最佳化的那样,就仅是一个光线追踪器而已。仅加入类型与行内宣告 (13.3 节)就可以让它变得两倍以上快。 Chapter 9 总结 (Summary)¶
Chapter 9 练习 (Exercises)¶
写一个程序来模拟这样的比赛。你的结果实际上有建议委员会每年选出 10 个最佳歌手吗?
脚注
第十章:宏¶Lisp 代码是由 Lisp 对象的列表来表示。2.3 节宣称这让 Lisp 可以写出可自己写程序的程序。本章将示范如何跨越表达式与代码的界线。 10.1 求值 (Eval)¶如何产生表达式是很直观的:调用 > (eval '(+ 1 2 3))
6
> (eval '(format t "Hello"))
Hello
NIL
如果这看起很熟悉的话,这是应该的。这就是我们一直交谈的那个 (defun our-toplevel ()
(do ()
(nil)
(format t "~%> ")
(print (eval (read)))))
也是因为这个原因,顶层也称为读取─求值─打印循环 (read-eval-print loop, REPL)。 调用
有许多更好的方法 (下一节叙述)来利用产生代码的这个可能性。当然 对于程序员来说, (defun eval (expr env)
(cond ...
((eql (car expr) 'quote) (cdr expr))
...
(t (apply (symbol-function (car expr))
(mapcar #'(lambda (x)
(eval x env))
(cdr expr))))))
许多表达式由预设子句 (default clause)来处理,预设子句获得 但是像 函数 > (coerce '(lambda (x) x) 'function)
#<Interpreted-Function BF9D96>
而如果你将 > (compile nil '(lambda (x) (+ x 2)))
#<Compiled-Function BF55BE>
NIL
NIL
由于 函数 10.2 宏 (Macros)¶写出能写程序的程序的最普遍方法是通过定义宏。宏是通过转换 (transformation)而实现的操作符。你通过说明你一个调用应该要翻译成什么,来定义一个宏。这个翻译称为宏展开(macro-expansion),宏展开由编译器自动完成。所以宏所产生的代码,会变成程序的一个部分,就像你自己输入的程序一样。 宏通常通过调用 (defmacro nil! (x)
(list 'setf x nil))
这定义了一个新的操作符,称为 > (nil! x)
NIL
> x
NIL
完全等同于输入表达式 要测试一个函数,我们调用它,但要测试一个宏,我们看它的展开式 (expansion)。 函数 > (macroexpand-1 '(nil! x))
(SETF X NIL)
T
一个宏调用可以展开成另一个宏调用。当编译器(或顶层)遇到一个宏调用时,它持续展开它,直到不可展开为止。 理解宏的秘密是理解它们是如何被实现的。在台面底下,它们只是转换成表达式的函数。举例来说,如果你传入这个形式 (lambda (expr)
(apply #'(lambda (x) (list 'setf x nil))
(cdr expr)))
它会返回 10.3 反引号 (Backquote)¶反引号读取宏 (read-macro)使得从模版 (templates)建构列表变得有可能。反引号广泛使用在宏定义中。一个平常的引用是键盘上的右引号 (apostrophe),然而一个反引号是一个左引号。(译注: open quote 左引号,closed quote 右引号)。它称作“反引号”是因为它看起来像是反过来的引号 (titled backwards)。 (译注: 反引号是键盘左上方数字 1 左边那个: 一个反引号单独使用时,等于普通的引号: > `(a b c)
(A B C)
和普通引号一样,单一个反引号保护其参数被求值。 反引号的优点是,在一个反引号表达式里,你可以使用 > (setf a 1 b 2)
2
> `(a is ,a and b is ,b)
(A IS 1 AND B IS 2)
通过使用反引号取代调用 (defmacro nil! (x)
`(setf ,x nil))
> (setf lst '(a b c))
(A B C)
> `(lst is ,lst)
(LST IS (A B C))
> `(its elements are ,@lst)
(ITS ELEMENTS ARE A B C)
> (let ((x 0))
(while (< x 10)
(princ x)
(incf x)))
0123456789
NIL
我们可以通过使用一个剩余参数 (rest parameter) ,搜集主体的表达式列表,来定义一个这样的宏,接着使用 comma-at 来扒开这个列表放至展开式里: (defmacro while (test &rest body)
`(do ()
((not ,test))
,@body))
10.4 示例:快速排序法(Example: Quicksort)¶图 10.1 包含了重度依赖宏的一个示例函数 ── 一个使用快速排序演算法 λ 来排序向量的函数。这个函数的工作方式如下: (defun quicksort (vec l r)
(let ((i l)
(j r)
(p (svref vec (round (+ l r) 2)))) ; 1
(while (<= i j) ; 2
(while (< (svref vec i) p) (incf i))
(while (> (svref vec j) p) (decf j))
(when (<= i j)
(rotatef (svref vec i) (svref vec j))
(incf i)
(decf j)))
(if (>= (- j l) 1) (quicksort vec l j)) ; 3
(if (>= (- r i) 1) (quicksort vec i r)))
vec)
图 10.1 快速排序。
每一次递归时,分割越变越小,直到向量完整排序为止。 在图 10.1 的实现里,接受一个向量以及标记欲排序范围的两个整数。这个范围当下的中间元素被选为主键 ( 除了我们前一节定义的 10.5 设计宏 (Macro Design)¶撰写宏是一种独特的程序设计,它有着独一无二的目标与问题。能够改变编译器所看到的东西,就像是能够重写它一样。所以当你开始撰写宏时,你需要像语言设计者一样思考。 本节快速给出宏所牵涉问题的概要,以及解决它们的技巧。作为一个例子,我们会定义一个称为 > (ntimes 10
(princ "."))
..........
NIL
下面是一个不正确的 (defmacro ntimes (n &rest body)
`(do ((x 0 (+ x 1)))
((>= x ,n))
,@body))
这个定义第一眼看起来可能没问题。在上面这个情况,它会如预期的工作。但实际上它在两个方面坏掉了。 一个宏设计者需要考虑的问题之一是,不小心引入的变量捕捉 (variable capture)。这发生在当一个在宏展开式里用到的变量,恰巧与展开式即将插入的语境里,有使用同样名字作为变量的情况。不正确的 > (let ((x 10))
(ntimes 5
(setf x (+ x 1)))
x)
10
如果 > (let ((x 10))
(do ((x 0 (+ x 1)))
((>= x 5))
(setf x (+ x 1)))
x)
最普遍的解法是不要使用任何可能会被捕捉的一般符号。取而代之的我们使用 gensym (8.4 小节)。因为 (defmacro ntimes (n &rest body)
(let ((g (gensym)))
`(do ((,g 0 (+ ,g 1)))
((>= ,g ,n))
,@body)))
但这个宏在另一问题上仍有疑虑: 多重求值 (multiple evaluation)。因为第一个参数被直接插入 > (let ((v 10))
(ntimes (setf v (- v 1))
(princ ".")))
.....
NIL
由于 如果我们看看宏调用所展开的表达式,就可以知道为什么: > (let ((v 10))
(do ((#:g1 0 (+ #:g1 1)))
((>= #:g1 (setf v (- v 1))))
(princ ".")))
每次迭代我们不是把迭代变量 (gensym 通常印出前面有 避免非预期的多重求值的方法是设置一个变量,在任何迭代前将其设为有疑惑的那个表达式。这通常牵扯到另一个 gensym: (defmacro ntimes (n &rest body)
(let ((g (gensym))
(h (gensym)))
`(let ((,h ,n))
(do ((,g 0 (+ ,g 1)))
((>= ,g ,h))
,@body))))
终于,这是一个 非预期的变量捕捉与多重求值是折磨宏的主要问题,但不只有这些问题而已。有经验后,要避免这样的错误与避免更熟悉的错误一样简单,比如除以零的错误。 你的 Common Lisp 实现是一个学习更多有关宏的好地方。借由调用展开至内置宏,你可以理解它们是怎么写的。下面是大多数实现对于一个 > (pprint (macroexpand-1 '(cond (a b)
(c d e)
(t f))))
(IF A
B
(IF C
(PROGN D E)
F))
函数 10.6 通用化引用 (Generalized Reference)¶由于一个宏调用可以直接在它出现的地方展开成代码,任何展开为 (defmacro cah (lst) `(car ,lst))
然后因为一个 > (let ((x (list 'a 'b 'c)))
(setf (cah x) 44)
x)
(44 B C)
撰写一个展开成一个 (defmacro incf (x &optional (y 1)) ; wrong
`(setf ,x (+ ,x ,y)))
但这是行不通的。这两个表达式不相等: (setf (car (push 1 lst)) (1+ (car (push 1 lst))))
(incf (car (push 1 lst)))
如果 Common Lisp 提供了 (define-modify-macro our-incf (&optional (y 1)) +)
另一版将元素推至列表尾端的 (define-modify-macro append1f (val)
(lambda (lst val) (append lst (list val))))
后者会如下工作: > (let ((lst '(a b c)))
(append1f lst 'd)
lst)
(A B C D)
顺道一提, 10.7 示例:实用的宏函数 (Example: Macro Utilities)¶6.4 节介绍了实用函数 (utility)的概念,一种像是构造 Lisp 的通用操作符。我们可以使用宏来定义不能写作函数的实用函数。我们已经见过几个例子: (defmacro for (var start stop &body body)
(let ((gstop (gensym)))
`(do ((,var ,start (1+ ,var))
(,gstop ,stop))
((> ,var ,gstop))
,@body)))
(defmacro in (obj &rest choices)
(let ((insym (gensym)))
`(let ((,insym ,obj))
(or ,@(mapcar #'(lambda (c) `(eql ,insym ,c))
choices)))))
(defmacro random-choice (&rest exprs)
`(case (random ,(length exprs))
,@(let ((key -1))
(mapcar #'(lambda (expr)
`(,(incf key) ,expr))
exprs))))
(defmacro avg (&rest args)
`(/ (+ ,@args) ,(length args)))
(defmacro with-gensyms (syms &body body)
`(let ,(mapcar #'(lambda (s)
`(,s (gensym)))
syms)
,@body))
(defmacro aif (test then &optional else)
`(let ((it ,test))
(if it ,then ,else)))
图 10.2: 实用宏函数 第一个 > (for x 1 8
(princ x))
12345678
NIL
这比写出等效的 (do ((x 1 (+ x 1)))
((> x 8))
(princ x))
这非常接近实际的展开式: (do ((x 1 (1+ x))
(#:g1 8))
((> x #:g1))
(princ x))
宏需要引入一个额外的变量来持有标记范围 (range)结束的值。 上面在例子里的 图 10.2 的第二个宏 (in (car expr) '+ '- '*)
我们可以改写成: (let ((op (car expr)))
(or (eql op '+)
(eql op '-)
(eql op '*)))
确实,第一个表达式展开后像是第二个,除了变量 下一个例子 (random-choice (turn-left) (turn-right))
会被展开为: (case (random 2)
(0 (turn-left))
(1 (turn-right)))
下一个宏 (let ((x (gensym)) (y (gensym)) (z (gensym)))
...)
我们可以写成 (with-gensyms (x y z)
...)
到目前为止,图 10.2 定义的宏,没有一个可以定义成函数。作为一个规则,写成宏是因为你不能将它写成函数。但这个规则有几个例外。有时候你或许想要定义一个操作符来作为宏,好让它在编译期完成它的工作。宏 > (avg 2 4 8)
14/3
是一个这种例子的宏。我们可以将 (defun avg (&rest args)
(/ (apply #'+ args) (length args)))
但它会需要在执行期找出参数的数量。只要我们愿意放弃应用 图 10.2 的最后一个宏是 (let ((val (calculate-something)))
(if val
(1+ val)
0))
我们可以写成 (aif (calculate-something)
(1+ it)
0)
小心使用 ( Use judiciously),预期的变量捕捉可以是一个无价的技巧。Common Lisp 本身在多处使用它: 举例来说 像这些宏明确演示了为何要撰写替你写程序的程序。一旦你定义了 如果仍对此怀疑,考虑看看如果你没有使用任何内置宏时,程序看起来会是怎么样。所有宏产生的展开式,你会需要用手产生。你也可以将这个问题用在另一方面。当你在撰写一个程序时,扪心自问,我需要撰写宏展开式吗?如果是的话,宏所产生的展开式就是你需要写的东西。 10.8 源自 Lisp (On Lisp)¶现在宏已经介绍过了,我们看过更多的 Lisp 是由超乎我们想像的 Lisp 写成。许多不是函数的 Common Lisp 操作符是宏,而他们全部用 Lisp 写成的。只有二十五个 Common Lisp 内置的操作符是特殊操作符。 John Foderaro 将 Lisp 称为“可程序的程序语言。” λ 通过撰写你自己的函数与宏,你将 Lisp 变成任何你想要的语言。 (我们会在 17 章看到这个可能性的图形化示范)无论你的程序适合何种形式,你确信你可以将 Lisp 塑造成适合它的语言。 宏是这个灵活性的主要成分之一。它们允许你将 Lisp 变得完全认不出来,但仍然用一种有原则且高效的方法来实作。在 Lisp 社区里,宏是个越来越感兴趣的主题。可以使用宏办到惊人之事是很清楚的,但更确信的是宏背后还有更多需要被探索。如果你想的话,可以通过你来发现。Lisp 永远将进化放在程序员手里。这是它为什么存活的原因。 Chapter 10 总结 (Summary)¶
Chapter 10 练习 (Exercises)¶
(a) ((C D) A Z)
(b) (X B C D)
(c) ((C D A) Z)
> (let ((n 2))
(nth-expr n (/ 1 0) (+ 1 2) (/ 1 0)))
3
> (let ((i 0) (n 4))
(n-of n (incf i)))
(1 2 3 4)
(defmacro push (obj lst)
`(setf ,lst (cons ,obj ,lst)))
举出一个不会与实际 push 做一样事情的函数调用例子。
> (let ((x 1))
(double x)
x)
2
脚注
第十一章:Common Lisp 对象系统¶Common Lisp 对象系统,或称 CLOS,是一组用来实现面向对象编程的操作集。由于它们有着同样的历史,通常将这些操作视为一个群组。 λ 技术上来说,它们与其他部分的 Common Lisp 没什么大不同: 11.1 面向对象编程 Object-Oriented Programming¶面向对象编程意味著程序组织方式的改变。这个改变跟已经发生过的处理器运算处理能力分配的变化雷同。在 1970 年代,一个多用户的计算机系统代表著,一个或两个大型机连接到大量的哑终端(dumb terminal)。现在更可能的是大量相互通过网络连接的工作站 (workstation)。系统的运算处理能力现在分布至个体用户上,而不是集中在一台大型的计算机上。 面向对象编程所带来的变革与上例非常类似,前者打破了传统程序的组织方式。不再让单一的程序去操作那些数据,而是告诉数据自己该做什么,程序隐含在这些新的数据“对象”的交互过程之中。 举例来说,假设我们要算出一个二维图形的面积。一个办法是写一个单独的函数,让它检查其参数的类型,然后视类型做处理,如图 11.1 所示。 (defstruct rectangle
height width)
(defstruct circle
radius)
(defun area (x)
(cond ((rectangle-p x)
(* (rectangle-height x) (rectangle-width x)))
((circle-p x)
(* pi (expt (circle-radius x) 2)))))
> (let ((r (make-rectangle)))
(setf (rectangle-height r) 2
(rectangle-width r) 3)
(area r))
6
图 11.1: 使用结构及函数来计算面积 使用 CLOS 我们可以写出一个等效的程序,如图 11.2 所示。在面向对象模型里,我们的程序被拆成数个独一无二的方法,每个方法为某些特定类型的参数而生。图 11.2 中的两个方法,隐性地定义了一个与图 11.1 相似作用的 (defclass rectangle ()
(height width))
(defclass circle ()
(radius))
(defmethod area ((x rectangle))
(* (slot-value x 'height) (slot-value x 'width)))
(defmethod area ((x circle))
(* pi (expt (slot-value x 'radius) 2)))
> (let ((r (make-instance 'rectangle)))
(setf (slot-value r 'height) 2
(slot-value r 'width) 3)
(area r))
6
图 11.2: 使用类型与方法来计算面积 通过这种方式,我们将函数拆成独一无二的方法,面向对象暗指继承 (inheritance) ── 槽(slot)与方法(method)皆有继承。在图 11.2 中,作为第二个参数传给 (defclass colored ()
(color))
(defclass colored-circle (circle colored)
())
当我们创造
从实践层面来看,面向对象编程代表著以方法、类、实例以及继承来组织程序。为什么你会想这么组织程序?面向对象方法的主张之一说这样使得程序更容易改动。如果我们想要改变 11.2 类与实例 (Class and Instances)¶在 4.6 节时,我们看过了创建结构的两个步骤:我们调用 (defclass circle ()
(radius center))
这个定义说明了 要创建这个类的实例,我们调用通用的 > (setf c (make-instance 'circle))
#<CIRCLE #XC27496>
要给这个实例的槽赋值,我们可以使用 > (setf (slot-value c 'radius) 1)
1
与结构的字段类似,未初始化的槽的值是未定义的 (undefined)。 11.3 槽的属性 (Slot Properties)¶传给 通过替一个槽定义一个访问器 (accessor),我们隐式地定义了一个可以引用到槽的函数,使我们不需要再调用 (defclass circle ()
((radius :accessor circle-radius)
(center :accessor circle-center)))
那我们能够分别通过 > (setf c (make-instance 'circle))
#<CIRCLE #XC5C726>
> (setf (circle-radius c) 1)
1
> (circle-radius c)
1
通过指定一个 要指定一个槽的缺省值,我们可以给入一个 (defclass circle ()
((radius :accessor circle-radius
:initarg :radius
:initform 1)
(center :accessor circle-center
:initarg :center
:initform (cons 0 0))))
现在当我们创建一个 > (setf c (make-instance 'circle :radius 3))
#<CIRCLE #XC2DE0E>
> (circle-radius c)
3
> (circle-center c)
(0 . 0)
注意 我们可以指定某些槽是共享的 ── 也就是每个产生出来的实例,共享槽的值都会是一样的。我们通过声明槽拥有 举例来说,假设我们想要模拟一群成人小报 (a flock of tabloids)的行为。(译注:可以看看什么是 tabloids。)在我们的模拟中,我们想要能够表示一个事实,也就是当一家小报采用一个头条时,其它小报也会跟进的这个行为。我们可以通过让所有的实例共享一个槽来实现。若 (defclass tabloid ()
((top-story :accessor tabloid-story
:allocation :class)))
那么如果我们创立两家小报,无论一家的头条是什么,另一家的头条也会是一样的: > (setf daily-blab (make-instance 'tabloid)
unsolicited-mail (make-instance 'tabloid))
#<TABLOID #x302000EFE5BD>
> (setf (tabloid-story daily-blab) 'adultery-of-senator)
ADULTERY-OF-SENATOR
> (tabloid-story unsolicited-mail)
ADULTERY-OF-SENATOR
译注: ADULTERY-OF-SENATOR 参议员的性丑闻。 若有给入 11.4 基类 (Superclasses)¶
(defclass graphic ()
((color :accessor graphic-color :initarg :color)
(visible :accessor graphic-visible :initarg :visible
:initform t)))
(defclass screen-circle (circle graphic) ())
则 访问器及 > (graphic-color (make-instance 'screen-circle
:color 'red :radius 3))
RED
我们可以使每一个 (defclass screen-circle (circle graphic)
((color :initform 'purple)))
现在 > (graphic-color (make-instance 'screen-circle))
PURPLE
11.5 优先级 (Precedence)¶我们已经看过类别是怎样能有多个基类了。当一个实例的方法同时属于这个实例所属的几个类时,Lisp 需要某种方式来决定要使用哪个方法。优先级的重点在于确保这一切是以一种直观的方式发生的。 每一个类别,都有一个优先级列表:一个将自身及自身的基类从最具体到最不具体所排序的列表。在目前看过的例子中,优先级还不是需要讨论的议题,但在更大的程序里,它会是一个需要考虑的议题。 以下是一个更复杂的类别层级: (defclass sculpture () (height width depth))
(defclass statue (sclpture) (subject))
(defclass metalwork () (metal-type))
(defclass casting (metalwork) ())
(defclass cast-statue (statue casting) ())
图 11.3 包含了一个表示 ![]() 图 11.3: 类别层级 要替一个类别建构一个这样的网络,从最底层用一个节点表示该类别开始。接著替类别最近的基类画上节点,其顺序根据 一个类别的优先级列表可以通过如下步骤,遍历对应的网络计算出来:
这个定义的结果之一(实际上讲的是规则 3)在优先级列表里,类别不会在其子类别出现前出现。 图 11.3 的箭头演示了一个网络是如何遍历的。由这个图所决定出的优先级列表为: 优先级的主要目的是,当一个通用函数 (generic function)被调用时,决定要用哪个方法。这个过程在下一节讲述。另一个优先级重要的地方是,当一个槽从多个基类继承时。408 页的备注解释了当这情况发生时的应用规则。 λ 11.6 通用函数 (Generic Functions)¶一个通用函数 (generic function) 是由一个或多个方法组成的一个函数。方法可用 (defmethod combine (x y)
(list x y))
现在 > (combine 'a 'b)
(A B)
到现在我们还没有做任何一般函数做不到的事情。一个通用函数不寻常的地方是,我们可以继续替它加入新的方法。 首先,我们定义一些可以让新的方法引用的类别: (defclass stuff () ((name :accessor name :initarg :name)))
(defclass ice-cream (stuff) ())
(defclass topping (stuff) ())
这里定义了三个类别: 现在下面是替 (defmethod combine ((ic ice-cream) (top topping))
(format nil "~A ice-cream with ~A topping."
(name ic)
(name top)))
在这次 而当一个通用函数被调用时, Lisp 是怎么决定要用哪个方法的?Lisp 会使用参数的类别与参数的特化匹配且优先级最高的方法。这表示若我们用 > (combine (make-instance 'ice-cream :name 'fig)
(make-instance 'topping :name 'treacle))
"FIG ice-cream with TREACLE topping"
但使用其他参数时,我们会得到我们第一次定义的方法: > (combine 23 'skiddoo)
(23 SKIDDOO)
因为第一个方法的两个参数皆没有特化,它永远只有最低优先权,并永远是最后一个调用的方法。一个未特化的方法是一个安全手段,就像 一个方法中,任何参数的组合都可以特化。在这个方法里,只有第一个参数被特化了: (defmethod combine ((ic ice-cream) x)
(format nil "~A ice-cream with ~A."
(name ic)
x))
若我们用一个 > (combine (make-instance 'ice-cream :name 'grape)
(make-instance 'topping :name 'marshmallow))
"GRAPE ice-cream with MARSHMALLOW topping"
然而若第一个参数是 > (combine (make-instance 'ice-cream :name 'clam)
'reluctance)
"CLAM ice-cream with RELUCTANCE"
当一个通用函数被调用时,参数决定了一个或多个可用的方法 (applicable methods)。如果在调用中的参数在参数的特化约定内,我们说一个方法是可用的。 如果没有可用的方法,我们会得到一个错误。如果只有一个,它会被调用。如果多于一个,最具体的会被调用。最具体可用的方法是由调用传入参数所属类别的优先级所决定的。由左往右审视参数。如果有一个可用方法的第一个参数,此参数特化给某个类,其类的优先级高于其它可用方法的第一个参数,则此方法就是最具体的可用方法。平手时比较第二个参数,以此类推。 [2] 在前面的例子里,很容易看出哪个是最具体的可用方法,因为所有的对象都是单继承的。一个 方法不需要在由 (defmethod combine ((x number) (y number))
(+ x y))
方法甚至可以对单一的对象做特化,用 (defmethod combine ((x (eql 'powder)) (y (eql 'spark)))
'boom)
单一对象特化的优先级比类别特化来得高。 方法可以像一般 Common Lisp 函数一样有复杂的参数列表,但所有组成通用函数方法的参数列表必须是一致的 (congruent)。参数的数量必须一致,同样数量的选择性参数(如果有的话),要嘛一起使用 (x) (a)
(x &optional y) (a &optional b)
(x y &rest z) (a b &key c)
(x y &key z) (a b &key c d)
而下列的参数列表对不是一致的: (x) (a b)
(x &optional y) (a &optional b c)
(x &optional y) (a &rest b)
(x &key x y) (a)
只有必要参数可以被特化。所以每个方法都可以通过名字及必要参数的特化独一无二地识别出来。如果我们定义另一个方法,有着同样的修饰符及特化,它会覆写掉原先的。所以通过说明 (defmethod combine ((x (eql 'powder)) (y (eql 'spark)))
'kaboom)
我们重定义了当 11.7 辅助方法 (Auxiliary Methods)¶方法可以通过如
这称为标准方法组合机制 (standard method combination)。在标准方法组合机制里,调用一个通用函数会调用
返回值为 辅助方法通过在 (defclass speaker () ())
(defmethod speak ((s speaker) string)
(format t "~A" string))
则使用 > (speak (make-instance 'speaker)
"I'm hungry")
I'm hungry
NIL
通过定义一个 (defclass intellectual (speaker) ())
(defmethod speak :before ((i intellectual) string)
(princ "Perhaps "))
(defmethod speak :after ((i intellectual) string)
(princ " in some sense"))
我们可以创建一个说话前后带有惯用语的演讲者: > (speak (make-instance 'intellectual)
"I am hungry")
Perhaps I am hungry in some sense
NIL
如同先前标准方法组合机制所述,所有的 (defmethod speak :before ((s speaker) string)
(princ "I think "))
无论是哪个 而在有 有了 (defclass courtier (speaker) ())
(defmethod speak :around ((c courtier) string)
(format t "Does the King believe that ~A?" string)
(if (eql (read) 'yes)
(if (next-method-p) (call-next-method))
(format t "Indeed, it is a preposterous idea. ~%"))
'bow)
当传给 > (speak (make-instance 'courtier) "kings will last")
Does the King believe that kings will last? yes
I think kings will last
BOW
> (speak (make-instance 'courtier) "kings will last")
Does the King believe that kings will last? no
Indeed, it is a preposterous idea.
BOW
记得由 11.8 方法组合机制 (Method Combination)¶在标准方法组合中,只有最具体的主方法会被调用(虽然它可以通过 用其它组合手段来定义方法也是有可能的 ── 举例来说,一个返回所有可用主方法的和的通用函数。操作符 (Operator)方法组合可以这么理解,想像它是 Lisp 表达式的求值后的结果,其中 Lisp 表达式的第一个元素是某个操作符,而参数是按照具体性调用可用主方法的结果。如果我们定义 (defun price (&rest args)
(+ (apply 〈most specific primary method〉 args)
.
.
.
(apply 〈least specific primary method〉 args)))
如果有可用的 我们可以指定一个通用函数的方法组合所要使用的类型,借由在 (defgeneric price (x)
(:method-combination +))
现在 (defclass jacket () ())
(defclass trousers () ())
(defclass suit (jacket trousers) ())
(defmethod price + ((jk jacket)) 350)
(defmethod price + ((tr trousers)) 200)
则可获得一件正装的价钱,也就是所有可用方法的总和: > (price (make-instance 'suit))
550
下列符号可以用来作为 + and append list max min nconc or progn
你也可以使用 一旦你指定了通用函数要用何种方法组合,所有替该函数定义的方法必须用同样的机制。而现在如果我们试著使用另个操作符( 11.9 封装 (Encapsulation)¶面向对象的语言通常会提供某些手段,来区别对象的表示法以及它们给外在世界存取的介面。隐藏实现细节带来两个优点:你可以改变实现方式,而不影响对象对外的样子,而你可以保护对象在可能的危险方面被改动。隐藏细节有时候被称为封装 (encapsulated)。 虽然封装通常与面向对象编程相关联,但这两个概念其实是没相干的。你可以只拥有其一,而不需要另一个。我们已经在 108 页 (译注: 6.5 小节。)看过一个小规模的封装例子。函数 在 Common Lisp 里,包是标准的手段来区分公开及私有的信息。要限制某个东西的存取,我们将它放在另一个包里,并且针对外部介面,仅输出需要用的名字。 我们可以通过输出可被改动的名字,来封装一个槽,但不是槽的名字。举例来说,我们可以定义一个 (defpackage "CTR"
(:use "COMMON-LISP")
(:export "COUNTER" "INCREMENT" "CLEAR"))
(in-package ctr)
(defclass counter () ((state :initform 0)))
(defmethod increment ((c counter))
(incf (slot-value c 'state)))
(defmethod clear ((c counter))
(setf (slot-value c 'state) 0))
在这个定义下,在包外部的代码只能够创造 如果你想要更进一步区别类的内部及外部介面,并使其不可能存取一个槽所存的值,你也可以这么做。只要在你将所有需要引用它的代码定义完,将槽的名字 unintern: (unintern 'state)
则没有任何合法的、其它的办法,从任何包来引用到这个槽。 λ 11.10 两种模型 (Two Models)¶面向对象编程是一个令人疑惑的话题,部分的原因是因为有两种实现方式:消息传递模型 (message-passing model)与通用函数模型 (generic function model)。一开始先有的消息传递。通用函数是广义的消息传递。 在消息传递模型里,方法属于对象,且方法的继承与槽的继承概念一样。要找到一个物体的面积,我们传给它一个 tell obj area
而这调用了任何对象 有时候我们需要传入额外的参数。举例来说,一个 (move obj 10)
消息传递模型的局限性变得清晰。在消息传递模型里,我们仅特化 (specialize) 第一个参数。 牵扯到多对象时,没有规则告诉方法该如何处理 ── 而对象回应消息的这个模型使得这更加难处理了。 在消息传递模型里,方法是对象所有的,而在通用函数模型里,方法是特别为对象打造的 (specialized)。 如果我们仅特化第一个参数,那么通用函数模型和消息传递模型就是一样的。但在通用函数模型里,我们可以更进一步,要特化几个参数就几个。这也表示了,功能上来说,消息传递模型是通用函数模型的子集。如果你有通用函数模型,你可以仅特化第一个参数来模拟出消息传递模型。 Chapter 11 总结 (Summary)¶
Chapter 11 练习 (Exercises)¶
(defclass a (c d) ...) (defclass e () ...)
(defclass b (d c) ...) (defclass f (h) ...)
(defclass c () ...) (defclass g (h) ...)
(defclass d (e f g) ...) (defclass h () ...)
使用这些函数(不要使用
脚注
第十二章:结构¶3.3 节中介绍了 Lisp 如何使用指针允许我们将任何值放到任何地方。这种说法是完全有可能的,但这并不一定都是好事。 例如,一个对象可以是它自已的一个元素。这是好事还是坏事,取决于程序员是不是有意这样设计的。 12.2 修改 (Modification)¶为什么要避免共享结构呢?之前讨论的共享结构问题仅仅是个智力练习,到目前为止,并没使我们在实际写程序的时候有什么不同。当修改一个被共享的结构时,问题出现了。如果两个列表共享结构,当我们修改了其中一个,另外一个也会无意中被修改。 上一节中,我们介绍了怎样构建一个是其它列表的尾端的列表: (setf whole (list 'a 'b 'c)
tail (cdr whole))
因为 > (setf (second tail ) 'e)
E
> tail
(B E)
> whole
(A B E)
同样的,如果两个列表共享同一个尾端,这种情况也会发生。 一次修改两个对象并不总是错误的。有时候这可能正是你想要的。但如果无意的修改了共享结构,将会引入一些非常微妙的 bug。Lisp 程序员要培养对共享结构的意识,并且在这类错误发生时能够立刻反应过来。当一个列表神秘的改变了的时候,很有可能是因为改变了其它与之共享结构的对象。 真正危险的不是共享结构,而是改变被共享的结构。为了安全起见,干脆避免对结构使用 当你调用别人写的函数的时候要加倍小心。除非你知道它内部的操作,否则,你传入的参数时要考虑到以下的情况: 1.它对你传入的参数可能会有破坏性的操作 2.你传入的参数可能被保存起来,如果你调用了一个函数,然后又修改了之前作为参数传入该函数的对象,那么你也就改变了函数已保存起来作为它用的对象[1]。 在这两种情况下,解决的方法是传入一个拷贝。 在 Common Lisp 中,一个函数调用在遍历列表结构 (比如, 12.3 示例:队列 (Example: Queues)¶共享结构并不是一个总让人担心的特性。我们也可以对其加以利用的。这一节展示了怎样用共享结构来表示队列。队列对象是我们可以按照数据的插入顺序逐个检出数据的仓库,这个规则叫做先进先出 (FIFO, first in, first out)。 用列表表示栈 (stack)比较容易,因为栈是从同一端插入和检出。而表示队列要困难些,因为队列的插入和检出是在不同端。为了有效的实现队列,我们需要找到一种办法来指向列表的两个端。 图 12.6 给出了一种可行的策略。它展示怎样表示一个含有 a,b,c 三个元素的队列。一个队列就是一对列表,最后那个 ![]() 图 12.6 一个队列的结构 (defun make-queue () (cons nil nil))
(defun enqueue (obj q)
(if (null (car q))
(setf (cdr q) (setf (car q) (list obj)))
(setf (cdr (cdr q)) (list obj)
(cdr q) (cdr (cdr q))))
(car q))
(defun dequeue (q)
(pop (car q)))
图 12.7 队列实现 图 12.7 中的代码实现了这一策略。其用法如下: > (setf q1 (make-queue))
(NIL)
> (progn (enqueue 'a q1)
(enqueue 'b q1)
(enqueue 'c q1))
(A B C)
现在, > q1
((A B C) C)
从队列中检出一些元素: > (dequeue q1)
A
> (dequeue q1)
B
> (enqueue 'd q1)
(C D)
12.4 破坏性函数 (Destructive Functions)¶Common Lisp 包含一些允许修改列表结构的函数。为了提高效率,这些函数是具有破坏性的。虽然它们可以回收利用作为参数传给它们的 比如, > (setf lst '(a r a b i a) )
(A R A B I A)
> (delete 'a lst )
(R B I)
> lst
(A R B I)
正如 (setf lst (delete 'a lst))
破坏性函数是怎样回收利用传给它们的列表的呢?比如,可以考虑 (defun nconc2 ( x y)
(if (consp x)
(progn
(setf (cdr (last x)) y)
x)
y))
我们找到第一个列表的最后一个 Cons 核 (cons cells),把它的 函数 > (mapcan #'list
'(a b c)
'(1 2 3 4))
( A 1 B 2 C 3)
这个函数可以定义如下: (defun our-mapcan (fn &rest lsts )
(apply #'nconc (apply #'mapcar fn lsts)))
使用 这类函数在处理某些问题的时候特别有用,比如,收集树在某层上的所有子结点。如果 (defun grandchildren (x)
(mapcan #'(lambda (c)
(copy-list (children c)))
(children x)))
这个函数调用 一个 (defun mappend (fn &rest lsts )
(apply #'append (apply #'mapcar fn lsts)))
如果使用 (defun grandchildren (x)
(mappend #'children (children x)))
12.5 示例:二叉搜索树 (Example: Binary Search Trees)¶在某些情况下,使用破坏性操作比使用非破坏性的显得更自然。第 4.7 节中展示了如何维护一个具有二分搜索格式的有序对象集 (或者说维护一个二叉搜索树 (BST))。第 4.7 节中给出的函数都是非破坏性的,但在我们真正使用BST的时候,这是一个不必要的保护措施。本节将展示如何定义更符合实际应用的具有破坏性的插入函数和删除函数。 图 12.8 展示了如何定义一个具有破坏性的 > (setf *bst* nil)
NIL
> (dolist (x '(7 2 9 8 4 1 5 12))
(setf *bst* (bst-insert! x *bst* #'<)))
NIL
(defun bst-insert! (obj bst <)
(if (null bst)
(make-node :elt obj)
(progn (bsti obj bst <)
bst)))
(defun bsti (obj bst <)
(let ((elt (node-elt bst)))
(if (eql obj elt)
bst
(if (funcall < obj elt)
(let ((l (node-l bst)))
(if l
(bsti obj l <)
(setf (node-l bst)
(make-node :elt obj))))
(let ((r (node-r bst)))
(if r
(bsti obj r <)
(setf (node-r bst)
(make-node :elt obj))))))))
图 12.8: 二叉搜索树:破坏性插入 你也可以为 BST 定义一个类似 push 的功能,但这超出了本书的范围。(好奇的话,可以参考第 409 页 「译者注:即备注 204 」 的宏定义。) 与 > (setf *bst* (bst-delete 2 *bst* #'<) )
#<7>
> (bst-find 2 *bst* #'<)
NIL
(defun bst-delete (obj bst <)
(if bst (bstd obj bst nil nil <))
bst)
(defun bstd (obj bst prev dir <)
(let ((elt (node-elt bst)))
(if (eql elt obj)
(let ((rest (percolate! bst)))
(case dir
(:l (setf (node-l prev) rest))
(:r (setf (node-r prev) rest))))
(if (funcall < obj elt)
(if (node-l bst)
(bstd obj (node-l bst) bst :l <))
(if (node-r bst)
(bstd obj (node-r bst) bst :r <))))))
(defun percolate! (bst)
(cond ((null (node-l bst))
(if (null (node-r bst))
nil
(rperc! bst)))
((null (node-r bst)) (lperc! bst))
(t (if (zerop (random 2))
(lperc! bst)
(rperc! bst)))))
(defun lperc! (bst)
(setf (node-elt bst) (node-elt (node-l bst)))
(percolate! (node-l bst)))
(defun rperc! (bst)
(setf (node-elt bst) (node-elt (node-r bst)))
(percolate! (node-r bst)))
图 12.9: 二叉搜索树:破坏性删除 译注: 此范例已被回报为错误的,一个修复的版本请造访这里。 12.6 示例:双向链表 (Example: Doubly-Linked Lists)¶普通的 Lisp 列表是单向链表,这意味着其指针指向一个方向:我们可以获取下一个元素,但不能获取前一个。在双向链表中,指针指向两个方向,我们获取前一个元素和下一个元素都很容易。这一节将介绍如何创建和操作双向链表。 图 12.10 展示了如何用结构来实现双向链表。将 (defstruct (dl (:print-function print-dl))
prev data next)
(defun print-dl (dl stream depth)
(declare (ignore depth))
(format stream "#<DL ~A>" (dl->list dl)))
(defun dl->list (lst)
(if (dl-p lst)
(cons (dl-data lst) (dl->list (dl-next lst)))
lst))
(defun dl-insert (x lst)
(let ((elt (make-dl :data x :next lst)))
(when (dl-p lst)
(if (dl-prev lst)
(setf (dl-next (dl-prev lst)) elt
(dl-prev elt) (dl-prev lst)))
(setf (dl-prev lst) elt))
elt))
(defun dl-list (&rest args)
(reduce #'dl-insert args
:from-end t :initial-value nil))
(defun dl-remove (lst)
(if (dl-prev lst)
(setf (dl-next (dl-prev lst)) (dl-next lst)))
(if (dl-next lst)
(setf (dl-prev (dl-next lst)) (dl-prev lst)))
(dl-next lst))
图 12.10: 构造双向链表 ![]() 图 12.11: 一个双向链表。 为了便于操作,我们为双向链表定义了一些实现类似 函数 几个普通列表可以共享同一个尾端。因为双向链表的尾端不得不指向它的前一个元素,所以不可能存在两个双向链表共享同一个尾端。如果 单向链表(普通列表)和双向链表另一个有趣的区别是,如何持有它们。我们使用普通列表的首端,来表示单向链表,如果将列表赋值给一个变量,变量可以通过保存指向列表第一个 函数 > (dl-list 'a 'b 'c)
#<DL (A B C)>
它使用了 (dl-insert 'a (dl-insert 'b (dl-insert 'c nil)) )
如果将 > (setf dl (dl-list 'a 'b))
#<DL (A B)>
> (setf dl (dl-insert 'c dl))
#<DL (C A B)>
> (dl-insert 'r (dl-next dl))
#<DL (R A B)>
> dl
#<DL (C R A B)>
最后, 12.7 环状结构 (Circular Structure)¶将列表结构稍微修改一下,就可以得到一个环形列表。存在两种环形列表。最常用的一种是其顶层列表结构是一个环的,我们把它叫做 构造一个单元素的 > (setf x (list 'a))
(A)
> (progn (setf (cdr x) x) nil)
NIL
这样 ![]() 图 12.12 环状列表。 如果 Lisp 试着打印我们刚刚构造的结构,将会显示 (a a a a a …… —— 无限个 > (setf *print-circle* t )
T
> x
#1=(A . #1#)
如果你需要,你也可以使用
(defun circular (lst)
(setf (cdr (last lst)) lst))
另外一种环状列表叫做 > (let ((y (list 'a )))
(setf (car y) y)
y)
#i=(#i#)
图 12.12 (右) 展示了其结构。这个 一个列表也可以既是 > (let ((c (cons 11)) )
(setf (car c) c
(cdr c) c)
c)
#1=(#1# . #1#)
很难想像这样的一个列表有什么用。实际上,了解环形列表的主要目的就是为了避免因为偶然因素构造出了环形列表,因为,将一个环形列表传给一个函数,如果该函数遍历这个环形列表,它将进入死循环。 环形结构的这种问题在列表以外的其他对象中也存在。比如,一个数组可以将数组自身当作其元素: > (setf *print-array* t )
T
> (let ((a (make-array 1)) )
(setf (aref a 0) a)
a)
#1=#(#1#)
实际上,任何可以包含元素的对象都可能包含其自身作为元素。 用 > (progn (defstruct elt
(parent nil ) (child nil) )
(let ((c (make-elt) )
(p (make-elt)) )
(setf (elt-parent c) p
(elt-child p) c)
c) )
#1=#S(ELT PARENT #S(ELT PARENT NIL CHILD #1#) CHILD NIL)
要实现像这样一个结构的打印函数 ( 12.8 常量结构 (Constant Structure)¶因为常量实际上是程序代码的一部分,所以我们也不应该修改他们,或者是不经意地写了自重写的代码。一个通过 (defun arith-op (x)
(member x '(+ - * /)))
如果被测试的符号是算术运算符,它的返回值将至少一个被引用列表的一部分。如果我们修改了其返回值, > (nconc (arith-op '*) '(as i t were))
(* / AS IT WERE)
那么我就会修改 > (arith-op 'as )
(AS IT WERE)
写一个返回常量结构的函数,并不一定是错误的。但当你考虑使用一个破坏性的操作是否安全的时候,你必须考虑到这一点。 有几个其它方法来实现 (defun arith-op (x)
(member x (list '+ '- '* '/)))
这里,使用 (defun arith-op (x)
(find x '(+ - * /)))
这一节讨论的问题似乎只与列表有关,但实际上,这个问题存在于任何复杂的对象中:数组,字符串,结构,实例等。你不应该逐字地去修改程序的代码段。 即使你想写自修改程序,通过修改常量来实现并不是个好办法。编译器将常量编译成了代码,破坏性的操作可能修改它们的参数,但这些都是没有任何保证的事情。如果你想写自修改程序,正确的方法是使用闭包 (见 6.5 节)。 Chapter 12 总结 (Summary)¶
Chapter 12 练习 (Exercises)¶
> (setf q (make-queue))
(NIL)
> (enqueue 'a q)
(A)
> (enqueue 'b q)
(A B)
> (dequeue q)
A
脚注
第十三章:速度¶Lisp 实际上是两种语言:一种能写出快速执行的程序,一种则能让你快速的写出程序。 在程序开发的早期阶段,你可以为了开发上的便捷舍弃程序的执行速度。一旦程序的结构开始固化,你就可以精炼其中的关键部分以使得它们执行的更快。 由于各个 Common Lisp 实现间的差异,很难针对优化给出通用的建议。在一个实现上使程序变快的修改也许在另一个实现上会使得程序变慢。这是难免的事儿。越强大的语言,离机器底层就越远,离机器底层越远,语言的不同实现沿着不同路径趋向它的可能性就越大。因此,即便有一些技巧几乎一定能够让程序运行的更快,本章的目的也只是建议而不是规定。 13.1 瓶颈规则 (The Bottleneck Rule)¶不管是什么实现,关于优化都可以整理出三点规则:它应该关注瓶颈,它不应该开始的太早,它应该始于算法。 也许关于优化最重要的事情就是要意识到,程序中的大部分执行时间都是被少数瓶颈所消耗掉的。 正如高德纳所说,“在一个与 I/O 无关 (Non-I/O bound) 的程序中,大部分的运行时间集中在大概 3% 的源代码中。” λ 优化程序的这一部分将会使得它的运行速度明显的提升;相反,优化程序的其他部分则是在浪费时间。 因此,优化程序时关键的第一步就是找到瓶颈。许多 Lisp 实现都提供性能分析器 (profiler) 来监视程序的运行并报告每一部分所花费的时间量。 为了写出最为高效的代码,性能分析器非常重要,甚至是必不可少的。 如果你所使用的 Lisp 实现带有性能分析器,那么请在进行优化时使用它。另一方面,如果实现没有提供性能分析器的话,那么你就不得不通过猜测来寻找瓶颈,而且这种猜测往往都是错的! 瓶颈规则的一个推论是,不应该在程序的初期花费太多的精力在优化上。高德纳对此深信不疑:“过早的优化是一切 (至少是大多数) 问题的源头。” λ 在刚开始写程序的时候,通常很难看清真正的瓶颈在哪,如果这个时候进行优化,你很可能是在浪费时间。优化也会使程序的修改变得更加困难,边写程序边优化就像是在用风干非常快的颜料来画画一样。 在适当的时候做适当的事情,可以让你写出更优秀的程序。 Lisp 的一个优点就是能让你用两种不同的工作方式来进行开发:很快地写出运行较慢的代码,或者,放慢写程序的速度,精雕细琢,从而得出运行得较快的代码。 在程序开发的初期阶段,工作通常在第一种模式下进行,只有当性能成为问题的时候,才切换到第二种模式。 对于非常底层的语言,比如汇编,你必须优化程序的每一行。但这么做会浪费你大部分的精力,因为瓶颈仅仅是其中很小的那部分代码。一个更加抽象的语言能够让你把主要精力集中在瓶颈上, 达到事半功倍的效果。 当真正开始优化的时候,还必须从最顶端入手。 在使用各种低层次的编码技巧 (low-level coding tricks) 之前,请先确保你已经使用了最为高效的算法。 这么做的潜在好处相当大 ── 甚至可能大到你都不再需要玩那些奇淫技巧。 当然本规则还是要和前一个规则保持平衡。 有些时候,关于算法的决策必须尽早进行。 13.2 编译 (Compilation)¶有五个参数可以控制代码的编译方式: speed (速度)代表编译器产生代码的速度; compilation-speed (编译速度)代表程序被编译的速度; safety (安全) 代表要对目标代码进行错误检查的数量; space (空间)代表目标代码的大小和内存需求量;最后, debug (调试)代表为了调试而保留的信息量。 Note 交互与解释 (INTERACTIVE VS. INTERPRETED) Lisp 是一种交互式语言 (Interactive Language),但是交互式的语言不必都是解释型的。早期的 Lisp 都通过解释器实现,因此认为 Lisp 的特质都依赖于它是被解释的想法就这么产生了。但这种想法是错误的:Common Lisp 既是编译型语言,又是解释型语言。 至少有两种 Common Lisp 实现甚至都不包含解释器。在这些实现中,输入到顶层的表达式在求值前会被编译。因此,把顶层叫做解释器的这种说法,不仅是落伍的,甚至还是错误的。 编译参数不是真正的变量。它们在声明中被分配从 (defun bottleneck (...)
(do (...)
(...)
(do (...)
(...)
(declare (optimize (speed 3) (safety 0)))
...)))
一般情况下,应该在代码写完并且经过完善测试之后,才考虑加上那么一句声明。 要让代码在任何情况下都尽可能地快,可以使用如下声明: (declaim (optimize (speed 3)
(compilation-speed 0)
(safety 0)
(debug 0)))
考虑到前面提到的瓶颈规则 [1] ,这种苛刻的做法可能并没有什么必要。 另一类特别重要的优化就是由 Lisp 编译器完成的尾递归优化。当 speed (速度)的权值最大时,所有支持尾递归优化的编译器都将保证对代码进行这种优化。 如果在一个调用返回时调用者中没有残余的计算,该调用就被称为尾递归。下面的代码返回列表的长度: (defun length/r (lst)
(if (null lst)
0
(1+ (length/r (cdr lst)))))
这个递归调用不是尾递归,因为当它返回以后,它的值必须传给 (defun length/rt (lst)
(labels ((len (lst acc)
(if (null lst)
acc
(len (cdr lst) (1+ acc)))))
(len lst 0)))
更准确地说,局部函数 出色的编译器能够将一个尾递归编译成一个跳转 (goto),因此也能将一个尾递归函数编译成一个循环。在典型的机器语言代码中,当第一次执行到表示 另一个利用函数调用抽象,却没有开销的方法是使函数内联编译。对于那些调用开销比函数体的执行代价还高的小型函数来说,这种技术非常有价值。例如,以下代码用于判断列表是否仅有一个元素: (declaim (inline single?))
(defun single? (lst)
(and (consp lst) (null (cdr lst))))
因为这个函数是在全局被声明为内联的,引用了 (defun foo (x)
(single? (bar x)))
当 (defun foo (x)
(let ((lst (bar x)))
(and (consp lst) (null (cdr lst)))))
内联编译有两个限制: 首先,递归函数不能内联。 其次,如果一个内联函数被重新定义,我们就必须重新编译调用它的任何函数,否则调用仍然使用原来的定义。 在一些早期的 Lisp 方言中,有时候会使用宏( 10.2 节)来避免函数调用。这种做法在 Common Lisp 中通常是没有必要的。 不同 Lisp 编译器的优化方式千差万别。
如果你想了解你的编译器为某个函数生成的代码,试着调用 13.3 类型声明 (Type Declarations)¶如果 Lisp 不是你所学的第一门编程语言,那么你也许会感到困惑,为什么这本书还没说到类型声明这件事来?毕竟,在很多流行的编程语言中,类型声明是必须要做的。 在不少编程语言里,你必须为每个变量声明类型,并且变量也只可以持有与该类型相一致的值。 这种语言被称为强类型(strongly typed) 语言。 除了给程序员们徒增了许多负担外,这种方式还限制了你能做的事情。 使用这种语言,很难写出那些需要多种类型的参数一起工作的函数,也很难定义出可以包含不同种类元素的数据结构。 当然,这种方式也有它的优势,比如无论何时当编译器碰到一个加法运算,它都能够事先知道这是一个什么类型的加法运算。如果两个参数都是整数类型,编译器可以直接在目标代码中生成一个固定 (hard-wire) 的整数加法运算。 正如 2.15 节所讲,Common Lisp 使用一种更加灵活的方式:显式类型 (manifest typing) [3] 。有类型的是值而不是变量。变量可以用于任何类型的对象。 当然,这种灵活性需要付出一定的速度作为代价。
由于 在某些时候,如果我们要执行的全都是整数的加法,那么每次查看参数类型的这种做法就说不上高效了。 Common Lisp 处理这种问题的方法是:让程序员尽可能地提示编译器。 比如说,如果我们提前就能知道某个加法运算的两个参数是定长数 (fixnums) ,那么就可以对此进行声明,这样编译器就会像 C 语言的那样为我们生成一个固定的整数加法运算。 因为显式类型也可以通过声明类型来生成高效的代码,所以强类型和显式类型两种方式之间的差别并不在于运行速度。 真正的区别是,在强类型语言中,类型声明是强制性的,而显式类型则不强加这样的要求。 在 Common Lisp 中,类型声明完全是可选的。它们可以让程序运行的更快,但(除非错误)不会改变程序的行为。 全局声明以 (declaim (type fixnum *count*))
在 ANSI Common Lisp 中,可以省略 (declaim (fixnum *count*))
局部声明通过 (defun poly (a b x)
(declare (fixnum a b x))
(+ (* a (expt x 2)) (* b x)))
在类型声明中的变量名指的就是该声明所在的上下文中的那个变量 ── 那个通过赋值可以改变它的值的变量。 你也可以通过 (defun poly (a b x)
(declare (fixnum a b x))
(the fixnum (+ (the fixnum (* a (the fixnum (expt x 2))))
(the fixnum (* b x)))))
看起来是不是很笨拙啊?幸运的是有两个原因让你很少会这样使用 Common Lisp 中有相当多的类型 ── 恐怕有无数种类型那么多,如果考虑到你可以自己定义新的类型的话。 类型声明只在少数情况下至关重要,可以遵照以下两条规则来进行:
类型声明对内容复杂的对象特别重要,这包括数组、结构和对象实例。这些声明可以在两个方面提升效率:除了可以让编译器来决定函数参数的类型以外,它们也使得这些对象可以在内存中更高效地表示。 如果对数组元素的类型一无所知的话,这些元素在内存中就不得不用一块指针来表示。但假如预先就知道数组包含的元素仅仅是 ── 比方说 ── 双精度浮点数 (double-floats),那么这个数组就可以用一组实际的双精度浮点数来表示。这样数组将占用更少的空间,因为我们不再需要额外的指针指向每一个双精度浮点数;同时,对数组元素的访问也将更快,因为我们不必沿着指针去读取和写元素。 ![]() 图 13.1:指定元素类型的效果 你可以通过 (setf x (vector 1.234d0 2.345d0 3.456d0)
y (make-array 3 :element-type 'double-float)
(aref y 0) 1.234d0
(aref y 1) 2.345d0
(aref y 2)3.456d0))
图 13.1 中的每一个矩形方格代表内存中的一个字 (a word of memory)。这两个数组都由未特别指明长度的头部 (header) 以及后续
三个元素的某种表示构成。对于 注意我们使用 除了在创建数组时指定元素的类型,你还应该在使用数组的代码中声明数组的维度以及它的元素类型。一个完整的向量声明如下: (declare (type (vector fixnum 20) v))
以上代码声明了一个仅含有定长数,并且长度固定为 (setf a (make-array '(1000 1000)
:element-type 'single-float
:initial-element 1.0s0))
(defun sum-elts (a)
(declare (type (simple-array single-float (1000 1000))
a))
(let ((sum 0.0s0))
(declare (type single-float sum))
(dotimes (r 1000)
(dotimes (c 1000)
(incf sum (aref a r c))))
sum))
图 13.2 对数组元素求和 最为通用的数组声明形式由数组类型以及紧接其后的元素类型和一个维度列表构成: (declare (type (simple-array fixnum (4 4)) ar))
图 13.2 展示了如何创建一个 1000×1000 的单精度浮点数数组,以及如何编写一个将该数组元素相加的函数。数组以行主序 (row-major order)存储,遍历时也应尽可能按此顺序进行。 我们将用 > (time (sum-elts a))
User Run Time = 0.43 seconds
1000000.0
如果我们把 sum-elts 中的类型声明去掉并重新编译它,同样的计算将花费超过5秒的时间: > (time (sum-elts a))
User Run Time = 5.17 seconds
1000000.0
类型声明的重要性 ── 特别是对数组和数来说 ── 怎么强调都不过分。上面的例子中,仅仅两行代码就可以让 13.4 避免垃圾 (Garbage Avoidance)¶Lisp 除了可以让你推迟考虑变量的类型以外,它还允许你推迟对内存分配的考虑。 在程序的早期阶段,暂时忽略内存分配和臭虫等问题,将有助于解放你的想象力。 等到程序基本固定下来以后,就可以开始考虑怎么减少动态分配,从而让程序运行得更快。 但是,并不是构造(consing)用得少的程序就一定快。
多数 Lisp 实现一直使用着差劲的垃圾回收器,在这些实现中,过多的内存分配容易让程序运行变得缓慢。
因此,『高效的程序应该尽可能地减少 本节介绍了几种方法,用于减少程序中的构造。 但构造数量的减少是否有利于加快程序的运行,这一点最终还是取决于实现。 最好的办法就是自己去试一试。 减少构造的办法有很多种。 有些办法对程序的修改非常少。 例如,最简单的方法就是使用破坏性函数。 下表罗列了一些常用的函数,以及这些函数对应的破坏性版本。
当确认修改列表是安全的时候,可以使用 即便你想完全摆脱构造,你也不必放弃在运行中 (on the fly)创建对象的可能性。 你需要做的是避免在运行中为它们分配空间和通过垃圾回收收回空间。通用方案是你自己预先分配内存块 (block of memory),以及明确回收用过的块。预先可能意味着在编译期或者某些初始化例程中。具体情况还应具体分析。 例如,当情况允许我们利用一个有限大小的堆栈时,我们可以让堆栈在一个已经分配了空间的向量中增长或缩减,而不是构造它。Common Lisp 内置支持把向量作为堆栈使用。如果我们传给 > (setf *print-array* t)
T
> (setf vec (make-array 10 :fill-pointer 2
:initial-element nil))
#(NIL NIL)
我们刚刚制造的向量对于操作序列的函数来说,仍好像只含有两个元素, > (length vec)
2
但它能够增长直到十个元素。因为 > (vector-push 'a vec)
2
> vec
#(NIL NIL A)
> (vector-pop vec)
A
> vec
#(NIL NIL)
当我们调用 使用带有填充指针的向量有一个缺点,就是它们不再是简单向量了。我们不得不使用 (defconstant dict (make-array 25000 :fill-pointer 0))
(defun read-words (from)
(setf (fill-pointer dict) 0)
(with-open-file (in from :direction :input)
(do ((w (read-line in nil :eof)
(read-line in nil :eof)))
((eql w :eof))
(vector-push w dict))))
(defun xform (fn seq) (map-into seq fn seq))
(defun write-words (to)
(with-open-file (out to :direction :output
:if-exists :supersede)
(map nil #'(lambda (x)
(fresh-line out)
(princ x out))
(xform #'nreverse
(sort (xform #'nreverse dict)
#'string<)))))
图 13.3 生成同韵字辞典 当应用涉及很长的序列时,你可以用 (setf v (map-into v #'1+ v))
图 13.3 展示了一个使用大向量应用的例子:一个生成简单的同韵字辞典 (或者更确切的说,一个不完全韵辞典)的程序。函数 a amoeba alba samba marimba...
结束是 ...megahertz gigahertz jazz buzz fuzz
利用填充指针和 在数值应用中要当心大数 (bignums)。大数运算需要构造,因此也就会比较慢。 即使程序的最后结果为大数,但是,通过调整计算,将中间结果保存在定长数中,这种优化也是有可能的。 另一个避免垃圾回收的方法是,鼓励编译器在栈上分配对象而不是在堆上。
如果你知道只是临时需要某个东西,你可以通过将它声明为 通过一个动态范围 (dynamic extent)变量声明,你告诉编译器,变量的值应该和变量保持相同的生命期。 什么时候值的生命期比变量长呢?这里有个例子: (defun our-reverse (lst)
(let ((rev nil))
(dolist (x lst)
(push x rev))
rev))
在 相比之下,考虑如下 (defun our-adjoin (obj lst &rest args)
(if (apply #'member obj lst args)
lst
(cons obj lst)))
在这个例子里,我们可以从函数的定义看出, (defun our-adjoin (obj lst &rest args)
(declare (dynamic-extent args))
(if (apply #'member obj lst args)
lst
(cons obj lst)))
那么编译器就可以 (但不是必须)在栈上为 13.5 示例: 存储池 (Example: Pools)¶对于涉及数据结构的应用,你可以通过在一个存储池 (pool)中预先分配一定数量的结构来避免动态分配。当你需要一个结构时,你从池中取得一份,当你用完后,再把它送回池中。为了演示存储池的使用,我们将快速的编写一段记录港口中船舶数量的程序原型 (prototype of a program),然后用存储池的方式重写它。 (defparameter *harbor* nil)
(defstruct ship
name flag tons)
(defun enter (n f d)
(push (make-ship :name n :flag f :tons d)
*harbor*))
(defun find-ship (n)
(find n *harbor* :key #'ship-name))
(defun leave (n)
(setf *harbor*
(delete (find-ship n) *harbor*)))
图 13.4 港口 图 13.4 中展示的是第一个版本。 全局变量 一个程序的初始版本这么写简直是棒呆了,但它会产生许多的垃圾。当这个程序运行时,它会在两个方面构造:当船只进入港口时,新的结构将会被分配;而 我们可以通过在编译期分配空间来消除这两种构造的源头 (sources of consing)。图 13.5 展示了程序的第二个版本,它根本不会构造。 (defconstant pool (make-array 1000 :fill-pointer t))
(dotimes (i 1000)
(setf (aref pool i) (make-ship)))
(defconstant harbor (make-hash-table :size 1100
:test #'eq))
(defun enter (n f d)
(let ((s (if (plusp (length pool))
(vector-pop pool)
(make-ship))))
(setf (ship-name s) n
(ship-flag s) f
(ship-tons s) d
(gethash n harbor) s)))
(defun find-ship (n) (gethash n harbor))
(defun leave (n)
(let ((s (gethash n harbor)))
(remhash n harbor)
(vector-push s pool)))
图 13.5 港口(第二版) 严格说来,新的版本仍然会构造,只是不在运行期。在第二个版本中, 我们使用存储池的行为实际上是肩负起内存管理的工作。这是否会让我们的程序更快仍取决于我们的 Lisp 实现怎样管理内存。总的说来,只有在那些仍使用着原始垃圾回收器的实现中,或者在那些对 GC 的不可预见性比较敏感的实时应用中才值得一试。 13.6 快速操作符 (Fast Operators)¶本章一开始就宣称 Lisp 是两种不同的语言。就某种意义来讲这确实是正确的。如果你仔细看过 Common Lisp 的设计,你会发现某些特性主要是为了速度,而另外一些主要为了便捷性。 例如,你可以通过三个不同的函数取得向量给定位置上的元素: 对于列表来说,你应该调用 另一对相似的函数是 使用 当被调函数有一个余留参数时,调用 (apply #'+ '(1 2 3))
写成如下可以更高效: (reduce #'+ '(1 2 3))
它不仅有助于调用正确的函数,还有助于按照正确的方式调用它们。余留、可选和关键字参数 是昂贵的。只使用普通参数,函数调用中的参量会被调用者简单的留在被调者能够找到的地方。但其他种类的参数涉及运行时的处理。关键字参数是最差的。针对内置函数,优秀的编译器采用特殊的办法把使用关键字参量的调用编译成快速代码 (fast code)。但对于你自己编写的函数,避免在程序中对速度敏感的部分使用它们只有好处没有坏处。另外,不把大量的参量都放到余留参数中也是明智的举措,如果这可以避免的话。 不同的编译器有时也会有一些它们独到优化。例如,有些编译器可以针对键值是一个狭小范围中的整数的 13.7 二阶段开发 (Two-Phase Development)¶在以速度至上的应用中,你也许想要使用诸如 C 或者汇编这样的低级语言来重写一个 Lisp 程序的某部分。你可以对用任何语言编写的程序使用这一技巧 ── C 程序的关键部分经常用汇编重写 ── 但语言越抽象,用两阶段(two phases)开发程序的好处就越明显。 Common Lisp 没有规定如何集成其他语言所编写的代码。这部分留给了实现决定,而几乎所有的实现都提供了某种方式来实现它。 使用一种语言编写程序然后用另一种语言重写它其中部分看起来可能是一种浪费。事实上,经验显示这是一种好的开发软件的方式。先针对功能、然后是速度比试着同时达成两者来的简单。 如果编程完全是一个机械的过程 ── 简单的把规格说明翻译为代码 ── 在一步中把所有的事情都搞定也许是合理的。但编程永远不是如此。不论规格说明多么精确, 编程总是涉及一定量的探索 ── 通常比任何人能预期到的还多的多。 一份好的规格说明,也许会让编程看起来像是简单的把它们翻译成代码的过程。这是一个普遍的误区。编程必定涉及探索,因为规格说明必定含糊不清。如果它们不含糊的话,它们就都算不上规格说明。 在其他领域,尽可能精准的规格说明也许是可取的。如果你要求一块金属被切割成某种形状,最好准确的说出你想要的。但这个规则不适用于软件,因为程序和规格说明由相同的东西构成:文本。你不可能编写出完全合意的规格说明。如果规格说明有那么精确的话,它们就变成程序了。 λ 对于存在着可观数量的探索的应用 (再一次,比任何人承认的还要多,将实现分成两个阶段是值得的。而且在第一阶段中你所使用的手段 (medium) 不必就是最后的那个。例如,制作铜像的标准方法是先从粘土开始。你先用粘土做一个塑像出来,然后用它做一个模子,在这个模子中铸造铜像。在最后的塑像中是没有丁点粘土的,但你可以从铜像的形状中认识到它发挥的作用。试想下从一开始就只用一块儿铜和一个凿子来制造这么个一模一样的塑像要多难啊!出于相同的原因,首先用 Lisp 来编写程序,然后用 C 改写它,要比从头开始就用 C 编写这个程序要好。 Chapter 13 总结 (Summary)¶
Chapter 13 练习 (Exercises)¶
(defun foo (x)
(if (zerop x)
0
(1+ (foo (1- x)))))
注意:你需要增加额外的参数。
(a) 在 5.7 节中的日期运算代码。
(b) 在 9.8 节中的光线跟踪器 (ray-tracer)。
脚注
第十四章:进阶议题¶本章是选择性阅读的。本章描述了 Common Lisp 里一些更深奥的特性。Common Lisp 像是一个冰山:大部分的功能对于那些永远不需要他们的多数用户是看不见的。你或许永远不需要自己定义包 (Package)或读取宏 (read-macros),但当你需要时,有些例子可以让你参考是很有用的。 14.1 类型标识符 (Type Specifiers)¶类型在 Common Lisp 里不是对象。举例来说,没有对象对应到 一个类型标识符是一个类型的名称。最简单的类型标识符是像是 一个类型实际上只是一个对象集合。这意味著有多少类型就有多少个对象的集合:一个无穷大的数目。我们可以用原子的类型标识符 (atomic type specifiers)来表示某些集合:比如 举例来说,如果 如果 (or vector (and list (not (satisfies circular?))))
某些原子的类型标识符也可以出现在复合类型标识符。要表示介于 1 至 100 的整数(包含),我们可以用: (integer 1 100)
这样的类型标识符用来表示一个有限的类型 (finite type)。 在一个复合类型标识符里,你可以通过在一个参数的位置使用 (simple-array fixnum (* *))
描述了指定给 (simple-array fixnum *)
描述了指定给 (simple-array fixnum)
若一个复合类型标识符没有传入参数,你可以使用一个原子。所以 如果有某些复合类型标识符你想重复使用,你可以使用 (deftype proseq ()
'(or vector (and list (not (satisfies circular?)))))
我们定义了 > (typep #(1 2) 'proseq)
T
如果你定义一个接受参数的类型标识符,参数会被视为 Lisp 形式(即没有被求值),与 (deftype multiple-of (n)
`(and integer (satisfies (lambda (x)
(zerop (mod x ,n))))))
(译注: 注意上面代码是使用反引号 定义了 (multiple-of n) 当成所有 > (type 12 '(multiple-of 4))
T
类型标识符会被直译 (interpreted),因此很慢,所以通常你最好定义一个函数来处理这类的测试。 14.2 二进制流 (Binary Streams)¶第 7 章曾提及的流有二进制流 (binary streams)以及字符流 (character streams)。一个二进制流是一个整数的来源及/或终点,而不是字符。你通过指定一个整数的子类型来创建一个二进制流 ── 当你打开流时,通常是用 关于二进制流的 I/O 函数仅有两个, (defun copy-file (from to)
(with-open-file (in from :direction :input
:element-type 'unsigned-byte)
(with-open-file (out to :direction :output
:element-type 'unsigned-byte)
(do ((i (read-byte in nil -1)
(read-byte in nil -1)))
((minusp i))
(declare (fixnum i))
(write-byte i out)))))
仅通过指定 (unsigned-byte 7)
来传给 14.3 读取宏 (Read-Macros)¶7.5 节介绍过宏字符 (macro character)的概念,一个对于 函数 Lisp 中最古老的读取宏之一是 (set-macro-character #\'
#'(lambda (stream char)
(list (quote quote) (read stream t nil t))))
当 译注: 现在我们明白了 (译注:困惑的话可以看看 read 的定义 ) 你可以(通过使用 你可以通过调用 (set-dispatch-macro-character #\# #\?
#'(lambda (stream char1 char2)
(list 'quote
(let ((lst nil))
(dotimes (i (+ (read stream t nil t) 1))
(push i lst))
(nreverse lst)))))
现在 > #?7
(1 2 3 4 5 6 7)
除了简单的宏字符,最常定义的宏字符是列表分隔符 (list delimiters)。另一个保留给用户的字符组是 (set-macro-character #\} (get-macro-character #\)))
(set-dispatch-macro-character #\# #\{
#'(lambda (stream char1 char2)
(let ((accum nil)
(pair (read-delimited-list #\} stream t)))
(do ((i (car pair) (+ i 1)))
((> i (cadr pair))
(list 'quote (nreverse accum)))
(push i accum)))))
这定义了一个这样形式 > #{2 7}
(2 3 4 4 5 6 7)
函数 如果你想要在定义一个读取宏的文件里使用该读取宏,则读取宏的定义应要包在一个 14.4 包 (Packages)¶一个包是一个将名字映对到符号的 Lisp 对象。当前的包总是存在全局变量 > (package-name *package*)
"COMMON-LISP-USER"
> (find-package "COMMON-LISP-USER")
#<Package "COMMON-LISP-USER" 4CD15E>
通常一个符号在读入时就被 interned 至当前的包里面了。函数 (symbol-package 'sym)
#<Package "COMMON-LISP-USER" 4CD15E>
有趣的是,这个表达式返回它该返回的值,因为表达式在可以被求值前必须先被读入,而读取这个表达式导致 > (setf sym 99)
99
现在我们可以创建及切换至一个新的包: > (setf *package* (make-package 'mine
:use '(common-lisp)))
#<Package "MINE" 63390E>
现在应该会听到诡异的背景音乐,因为我们来到一个不一样的世界了:
在这里 MINE> sym
Error: SYM has no value
为什么会这样?因为上面我们设为 99 的 MINE> common-lisp-user::sym
99
所以有着相同打印名称的不同符号能够在不同的包内共存。可以有一个 包也提供了信息隐藏的手段。程序应通过函数与变量的名字来参照它们。如果你不让一个名字在你的包之外可见的话,那么另一个包中的代码就无法使用或者修改这个名字所参照的对象。 通常使用两个冒号作为包的前缀也是很差的风格。这么做你就违反了包本应提供的模块性。如果你不得不使用一个双冒号来参照到一个符号,这是因为某人根本不想让你用。 通常我们应该只参照被输出 ( exported )的符号。如果我们回到用户包里,并输出一个被 interned 的符号, MINE> (in-package common-lisp-user)
#<Package "COMMON-LISP-USER" 4CD15E>
> (export 'bar)
T
> (setf bar 5)
5
我们使这个符号对于其它的包是可视的。现在当我们回到 > (in-package mine)
#<Package "MINE" 63390E>
MINE> common-lisp-user:bar
5
通过把 MINE> (import 'common-lisp-user:bar)
T
MINE> bar
5
在输入 要是已经有一个了怎么办?在这种情况下, MINE> (import 'common-lisp-user::sym)
Error: SYM is already present in MINE.
在此之前,当我们试着在 另一个方法来获得别的包内符号的存取权是使用( MINE> (use-package 'common-lisp-user)
T
现在所有由用户包 (译注: common-lisp-user 包)所输出的符号,可以不需要使用任何限定符在 含有自带操作符及变量名字的包叫做 MINE> #'cons
#<Compiled-Function CONS 462A3E>
在编译后的代码中, 通常不会像这样在顶层进行包的操作。更常见的是包的调用会包含在源文件里。通常,只要把 这种由包所提供的模块性实际上有点奇怪。我们不是对象的模块 (modules),而是名字的模块。 每一个使用了 14.5 Loop 宏 (The Loop Facility)¶
如果你是曾经计划某天要理解 这个宏唯一的实际定义是它的实现方式,而唯一可以理解它(如果有人可以理解的话)的方法是通过实例。ANSI 标准讨论 第一个关于 一个
我们会看几个 举例来说,最简单的 > (loop for x from 0 to 9
do (princ x))
0123456789
NIL
这个
贡献代码至前两个阶段,导致
贡献代码给主体。 一个更通用的 > (loop for x = 8 then (/ x 2)
until (< x 1)
do (princ x))
8421
NIL
你可以使用 > (loop for x from 1 to 4
and y from 1 to 4
do (princ (list x y)))
(1 1)(2 2)(3 3)(4 4)
NIL
要不然有多重 另一件在迭代代码通常会做的事是累积某种值。举例来说: > (loop for x in '(1 2 3 4)
collect (1+ x))
(2 3 4 5)
在 在这个情况里, 这是返回一个特定值的第一个例子。有用来明确指定返回值的子句,但没有这些子句时,一个
> (loop for x from 1 to 5
collect (random 10))
(3 8 6 5 0)
这里我们获得了一个含五个随机数的列表。这跟我们定义过的 一个 (defun even/odd (ns)
(loop for n in ns
if (evenp n)
collect n into evens
else collect n into odds
finally (return (values evens odds))))
一个 一个 (defun sum (n)
(loop for x from 1 to n
sum x))
(defun most (fn lst)
(if (null lst)
(values nil nil)
(let* ((wins (car lst))
(max (funcall fn wins)))
(dolist (obj (cdr lst))
(let ((score (funcall fn obj)))
(when (> score max)
(setf wins obj
max score))))
(values wins max))))
(defun num-year (n)
(if (< n 0)
(do* ((y (- yzero 1) (- y 1))
(d (- (year-days y)) (- d (year-days y))))
((<= d n) (values y (- n d))))
(do* ((y yzero (+ y 1))
(prev 0 d)
(d (year-days y) (+ d (year-days y))))
((> d n) (values y (- n prev))))))
图 14.1 不使用 loop 的迭代函数 (defun most (fn lst)
(if (null lst)
(values nil nil)
(loop with wins = (car lst)
with max = (funcall fn wins)
for obj in (cdr lst)
for score = (funcall fn obj)
when (> score max)
(do (setf wins obj
max score)
finally (return (values wins max))))))
(defun num-year (n)
(if (< n 0)
(loop for y downfrom (- yzero 1)
until (<= d n)
sum (- (year-days y)) into d
finally (return (values (+ y 1) (- n d))))
(loop with prev = 0
for y from yzero
until (> d n)
do (setf prev d)
sum (year-days y) into d
finally (return (values (- y 1)
(- n prev))))))
图 14.2 使用 loop 的迭代函数 一个 (loop for y = 0 then z
for x from 1 to 5
sum 1 into z
finally (return y z))
(loop for x from 1 to 5
for y = 0 then z
sum 1 into z
finally (return y z))
它们看起来够简单 ── 每一个有四个子句。但它们返回同样的值吗?它们返回的值多少?你若试着在标准中想找答案将徒劳无功。每一个 由于这类原因,使用 14.6 状况 (Conditions)¶在 Common Lisp 里,状况 (condition)包括了错误以及其它可能在执行期发生的情况。当一个状况被捕捉时 (signalled),相应的处理程序 (handler)会被调用。处理错误状况的缺省处理程序通常会调用一个中断循环 (break-loop)。但 Common Lisp 提供了多样的操作符来捕捉及处理错误。要覆写缺省的处理程序,甚至是自己写一个新的处理程序也是有可能的。 多数的程序员不会直接处理状况。然而有许多更抽象的操作符使用了状况,而要了解这些操作符,知道背后的原理是很有用的。 Common lisp 有数个操作符用来捕捉错误。最基本的是 > (error "Your report uses ~A as a verb." 'status)
Error: Your report uses STATUS as a verb
Options: :abort, :backtrace
>>
如上所示,除非这样的状况被处理好了,不然执行就会被打断。 用来捕捉错误的更抽象操作符包括了 > (ecase 1 (2 3) (4 5))
Error: No applicable clause
Options: :abort, :backtrace
>>
普通的
> (let ((x '(a b c)))
(check-type (car x) integer "an integer")
x)
Error: The value of (CAR X), A, should be an integer.
Options: :abort, :backtrace, :continue
>> :continue
New value of (CAR X)? 99
(99 B C)
>
在这个例子里, 这个宏是用更通用的 > (let ((sandwich '(ham on rye)))
(assert (eql (car sandwich) 'chicken)
((car sandwich))
"I wanted a ~A sandwich." 'chicken)
sandwich)
Error: I wanted a CHICKEN sandwich.
Options: :abort, :backtrace, :continue
>> :continue
New value of (CAR SANDWICH)? 'chicken
(CHICKEN ON RYE)
要建立新的处理程序也是可能的,但大多数程序员只会间接的利用这个可能性,通过使用像是 举例来说,如果在某个时候,你想要用户能够输入一个表达式,但你不想要在输入是语法上不合时中断执行,你可以这样写: (defun user-input (prompt)
(format t prompt)
(let ((str (read-line)))
(or (ignore-errors (read-from-string str))
nil)))
若输入包含语法错误时,这个函数仅返回 > (user-input "Please type an expression")
Please type an expression> #%@#+!!
NIL
脚注
第十五章:示例:推论¶接下来三章提供了大量的 Lisp 程序例子。选择这些例子来说明那些较长的程序所采取的形式,和 Lisp 所擅长解决的问题类型。 在这一章中我们将要写一个基于一组 15.1 目标 (The Aim)¶在这个程序中,我们将用一种熟悉的形式来表示信息:包含单个判断式,以及跟在之后的零个或多个参数所组成的列表。要表示 Donald 是 Nancy 的家长,我们可以这样写: (parent donald nancy)
事实上,我们的程序是要表示一些从已有的事实作出推断的规则。我们可以这样来表示规则: (<- head body)
其中, (<- (child ?x ?y) (parent ?y ?x))
表示:如果 y 是 x 的家长,那么 x 是 y 的孩子;更恰当地说,我们可以通过证明 可以把规则中的 body 部分(if-part) 写成一个复杂的表达式,其中包含 (<- (father ?x ?y) (and (parent ?x ?y) (male ?x)))
一些规则可能依赖另一些规则所产生的事实。比如,我们写的第一个规则是为了证明 (<- (daughter ?x ?y) (and (child ?x ?y) (female ?x)))
然后使用它来证明 表达式的证明可以回溯任意数量的规则,只要它最终结束于给出的已知事实。这个过程有时候被称为反向链接 (backward-chaining)。之所以说 反向 (backward) 是因为这一类推论先考虑 head 部分,这是为了在继续证明 body 部分之前检查规则是否有效。链接 (chaining) 来源于规则之间的依赖关系,从我们想要证明的内容到我们的已知条件组成一个链接 (尽管事实上它更像一棵树)。 λ 15.2 匹配 (Matching)¶我们需要有一个函数来做模式匹配以完成我们的反向链接 (back-chaining) 程序,这个函数能够比较两个包含变量的列表,它会检查在给变量赋值后是否可以使两个列表相等。举例,如果 (p ?x ?y c ?x)
(p a b c a)
当 (p ?x b ?y a)
(p ?y b c a)
当 我们有一个 (defun match (x y &optional binds)
(cond
((eql x y) (values binds t))
((assoc x binds) (match (binding x binds) y binds))
((assoc y binds) (match x (binding y binds) binds))
((var? x) (values (cons (cons x y) binds) t))
((var? y) (values (cons (cons y x) binds) t))
(t
(when (and (consp x) (consp y))
(multiple-value-bind (b2 yes)
(match (car x) (car y) binds)
(and yes (match (cdr x) (cdr y) b2)))))))
(defun var? (x)
(and (symbolp x)
(eql (char (symbol-name x) 0) #\?)))
(defun binding (x binds)
(let ((b (assoc x binds)))
(if b
(or (binding (cdr b) binds)
(cdr b)))))
图 15.1: 匹配函数。 > (match '(p a b c a) '(p ?x ?y c ?x))
((?Y . B) (?X . A))
T
> (match '(p ?x b ?y a) '(p ?y b c a))
((?Y . C) (?X . ?Y))
T
> (match '(a b c) '(a a a))
NIL
当 > (match '(p ?x) '(p ?x))
NIL
T
如果
下面是一个例子,按顺序来说明以上六种情况: > (match '(p ?v b ?x d (?z ?z))
'(p a ?w c ?y ( e e))
'((?v . a) (?w . b)))
((?Z . E) (?Y . D) (?X . C) (?V . A) (?W . B))
T
> (match '(?x a) '(?y ?y))
((?Y . A) (?X . ?Y))
T
先匹配 15.3 回答查询 (Answering Queries)¶在介绍了绑定的概念之后,我们可以更准确的说一下我们的程序将要做什么:它得到一个可能包含变量的表达式,根据我们给定的事实和规则返回使它正确的所有绑定。比如,我们只有下面这个事实: (parent donald nancy)
然后我们想让程序证明: (parent ?x ?y)
它会返回像下面这样的表达: (((?x . donald) (?y . nancy)))
它告诉我们只有一个可以让这个表达式为真的方法: 在通往目标的路上,我们已经有了一个的重要部分:一个匹配函数。 下面是用来定义规则的一段代码: (defvar *rules* (make-hash-table))
(defmacro <- (con &optional ant)
`(length (push (cons (cdr ',con) ',ant)
(gethash (car ',con) *rules*))))
图 15.2 定义规则 规则将被包含于一个叫做 我们将要使用同一个宏 > (<- (parent donald nancy))
1
> (<- (child ?x ?y) (parent ?y ?x))
1
调用 下面是我们的推论程序所需的大多数代码: (defun prove (expr &optional binds)
(case (car expr)
(and (prove-and (reverse (cdr expr)) binds))
(or (prove-or (cdr expr) binds))
(not (prove-not (cadr expr) binds))
(t (prove-simple (car expr) (cdr expr) binds))))
(defun prove-simple (pred args binds)
(mapcan #'(lambda (r)
(multiple-value-bind (b2 yes)
(match args (car r)
binds)
(when yes
(if (cdr r)
(prove (cdr r) b2)
(list b2)))))
(mapcar #'change-vars
(gethash pred *rules*))))
(defun change-vars (r)
(sublis (mapcar #'(lambda (v) (cons v (gensym "?")))
(vars-in r))
r))
(defun vars-in (expr)
(if (atom expr)
(if (var? expr) (list expr))
(union (vars-in (car expr))
(vars-in (cdr expr)))))
图 15.3: 推论。 上面代码中的 > (prove-simple 'parent '(donald nancy) nil)
(NIL)
> (prove-simple 'child '(?x ?y) nil)
(((#:?6 . NANCY) (#:?5 . DONALD) (?Y . #:?5) (?X . #:?6)))
以上两个返回值指出有一种方法可以证明我们的问题。(一个失败的证明将返回 nil。)第一个例子产生了一组空的绑定,第二个例子产生了这样的绑定: 顺便说一句,这是一个很好的例子来实践 2.13 节提出的观点。因为我们用函数式的风格来写这个程序,所以可以交互式地测试每一个函数。 第二个例子返回的值里那些 gensyms 是怎么回事?如果我们打算使用含有变量的规则,我们需要避免两个规则恰好包含相同的变量。如果我们定义如下两条规则: (<- (child ?x ?y) (parent ?y ?x))
(<- (daughter ?y ?x) (and (child ?y ?x) (female ?y)))
第一条规则要表达的意思是:对于任何的 如果我们使用上面所写的规则,它们将不会按预期的方式工作。如果我们尝试证明“ a 是 b 的女儿”,匹配到第二条规则的 head 部分时会将 > (match '(child ?y ?x)
'(child ?x ?y)
'((?y . a) (?x . b)))
NIL
为了保证一条规则中的变量只表示规则中各参数之间的关系,我们用 gensyms 来代替规则中的所有变量。这就是 现在只剩下定义用以证明复杂表达式的函数了。下面就是需要的函数: (defun prove-and (clauses binds)
(if (null clauses)
(list binds)
(mapcan #'(lambda (b)
(prove (car clauses) b))
(prove-and (cdr clauses) binds))))
(defun prove-or (clauses binds)
(mapcan #'(lambda (c) (prove c binds))
clauses))
(defun prove-not (clause binds)
(unless (prove clause binds)
(list binds)))
图 15.4 逻辑操作符 (Logical operators) 操作一个
现在我们有了一个可以工作的程序,但它不是很友好。必须要解析 (defmacro with-answer (query &body body)
(let ((binds (gensym)))
`(dolist (,binds (prove ',query))
(let ,(mapcar #'(lambda (v)
`(,v (binding ',v ,binds)))
(vars-in query))
,@body))))
图 15.5 介面宏 (Interface macro) 它接受一个 > (with-answer (parent ?x ?y)
(format t "~A is the parent of ~A.~%" ?x ?y))
DONALD is the parent of NANCY.
NIL
这个宏帮我们做了解析绑定的工作,同时为我们在程序中使用 (with-answer (p ?x ?y)
(f ?x ?y))
;;将被展开成下面的代码
(dolist (#:g1 (prove '(p ?x ?y)))
(let ((?x (binding '?x #:g1))
(?y (binding '?y #:g1)))
(f ?x ?y)))
图 15.6: with-answer 调用的展开式 下面是使用它的一个例子: (<- (parent donald nancy))
(<- (parent donald debbie))
(<- (male donald))
(<- (father ?x ?y) (and (parent ?x ?y) (male ?x)))
(<- (= ?x ?y))
(<- (sibling ?x ?y) (and (parent ?z ?x)
(parent ?z ?y)
(not (= ?x ?y))))
;;我们可以像下面这样做出推论
> (with-answer (father ?x ?y)
(format t "~A is the father of ~A.~%" ?x ?y))
DONALD is the father of DEBBIE.
DONALD is the father of NANCY.
NIL
> (with-answer (sibling ?x ?y))
(format t "~A is the sibling of ~A.~%" ?x ?y))
DEBBLE is the sibling of NANCY.
NANCY is the sibling of DEBBIE.
NIL
图 15.7: 使用中的程序 15.4 分析 (Analysis)¶看上去,我们在这一章中写的代码,是用简单自然的方式去实现这样一个程序。事实上,它的效率非常差。我们在这里是其实是做了一个解释器。我们能够把这个程序做得像一个编译器。 这里做一个简单的描述。基本的思想是把整个程序打包到两个宏 听上去好像比我们已经写的这个程序复杂很多,但其实可能只是长了两三倍。想要学习这种技术的读者可以看 On Lisp 或者 Paradigms of Artificial Intelligence Programming ,这两本书有一些使用这种风格写的示例程序。 第十六章:示例:生成 HTML¶本章的目标是完成一个简单的 HTML 生成器 —— 这个程序可以自动生成一系列包含超文本链接的网页。除了介绍特定 Lisp 技术之外,本章还是一个典型的自底向上编程(bottom-up programming)的例子。 我们以一些通用 HTML 实用函数作为开始,继而将这些例程看作是一门编程语言,从而更好地编写这个生成器。 16.1 超文本标记语言 (HTML)¶HTML (HyperText Markup Language,超文本标记语言)用于构建网页,是一种简单、易学的语言。本节就对这种语言作概括性介绍。 当你使用网页浏览器阅览网页时,浏览器从远程服务器获取 HTML 文件,并将它们显示在你的屏幕上。每个 HTML 文件都包含任意多个标签(tag),这些标签相当于发送给浏览器的指令。 ![]() 图 16.1 一个 HTML 文件 图 16.1 给出了一个简单的 HTML 文件,图 16.2 展示了这个 HTML 文件在浏览器里显示时大概是什么样子。 ![]() 图 16.2 一个网页 注意在尖角括号之间的文本并没有被显示出来,这些用尖角括号包围的文本就是标签。 HTML 的标签分为两种,一种是成双成对地出现的: <tag>...</tag>
第一个标签标志着某种情景(environment)的开始,而第二个标签标志着这种情景的结束。
这种标签的一个例子是 另外一些成双成对出现的标签包括:创建带编号列表的 被 一个像 <a href="foo.html">
这样的标签,就标识了一个指向另一个 HTML 文件的链接,其中这个 HTML 文件和当前网页的文件夹相同。
当点击这个链接时,浏览器就会获取并显示 当然,链接并不一定都要指向相同文件夹下的 HTML 文件,实际上,一个链接可以指向互联网的任何一个文件。 和成双成对出现的标签相反,另一种标签没有结束标记。
在图 16.1 里有一些这样的标签,包括:创建一个新文本行的 HTML 还有不少其他的标签,但是本章要用到的标签,基本都包含在图 16.1 里了。 16.2 HTML 实用函数 (HTML Utilities)¶(defmacro as (tag content)
`(format t "<~(~A~)>~A</~(~A~)>"
',tag ,content ',tag))
(defmacro with (tag &rest body)
`(progn
(format t "~&<~(~A~)>~%" ',tag)
,@body
(format t "~&</~(~A~)>~%" ',tag)))
(defmacro brs (&optional (n 1))
(fresh-line)
(dotimes (i n)
(princ "<br>"))
(terpri))
图 16.3 标签生成例程 本节会定义一些生成 HTML 的例程。
图 16.3 包含了三个基本的、生成标签的例程。
所有例程都将它们的输出发送到 宏 > (as center "The Missing Lambda")
<center>The Missing Lambda</center>
NIL
> (with center
(princ "The Unbalanced Parenthesis"))
<center>
The Unbalanced Parenthesis
</center>
NIL
两个宏都使用了 除此之外, 图 16.3 中的最后一个例程 (defun html-file (base)
(format nil "~(~A~).html" base))
(defmacro page (name title &rest body)
(let ((ti (gensym)))
`(with-open-file (*standard-output*
(html-file ,name)
:direction :output
:if-exists :supersede)
(let ((,ti ,title))
(as title ,ti)
(with center
(as h2 (string-upcase ,ti)))
(brs 3)
,@body))))
图 16.4 HTML 文件生成例程 图 16.4 包含用于生成 HTML 文件的例程。
第一个函数根据给定的符号(symbol)返回一个文件名。
在一个实际应用中,这个函数可能会返回指向某个特定文件夹的路径(path)。
目前来说,这个函数只是简单地将 宏 6.7 小节展示了如何临时性地绑定一个特殊变量。
在 113 页的例子中,我们在
如果我们调用 (page 'paren "The Unbalanced Parenthesis"
(princ "Something in his expression told her..."))
这会产生一个名为 <title>The Unbalanced Parenthesis</title>
<center>
<h2>THE UNBALANCED PARENTHESIS</h2>
</center>
<br><br><br>
Something in his expression told her...
除了 (defmacro with-link (dest &rest body)
`(progn
(format t "<a href=\"~A\">" (html-file ,dest))
,@body
(princ "</a>")))
(defun link-item (dest text)
(princ "<li>")
(with-link dest
(princ text)))
(defun button (dest text)
(princ "[ ")
(with-link dest
(princ text))
(format t " ]~%"))
图 16.5 生成链接的例程 图片 16.5 给出了用于生成链接的例程。
> (with-link 'capture
(princ "The Captured Variable"))
<a href="capture.html">The Captured Variable</a>
"</a>"
> (link-item 'bq "Backquote!")
<li><a href="bq.html">Backquote!</a>
"</a>"
最后, > (button 'help "Help")
[ <a href="help.html">Help</a> ]
NIL
16.3 迭代式实用函数 (An Iteration Utility)¶在这一节,我们先暂停一下编写 HTML 生成器的工作,转到编写迭代式例程的工作上来。 你可能会问,怎样才能知道,什么时候应该编写主程序,什么时候又应该编写子例程? 实际上,这个问题,没有答案。 通常情况下,你总是先开始写一个程序,然后发现需要写一个新的例程,于是你转而去编写新例程,完成它,接着再回过头去编写原来的程序。 时间关系,要在这里演示这个开始-完成-又再开始的过程是不太可能的,这里只展示这个迭代式例程的最终形态,需要注意的是,这个程序的编写并不如想象中的那么简单。 程序通常需要经历多次重写,才会变得简单。 (defun map3 (fn lst)
(labels ((rec (curr prev next left)
(funcall fn curr prev next)
(when left
(rec (car left)
curr
(cadr left)
(cdr left)))))
(when lst
(rec (car lst) nil (cadr lst) (cdr lst)))))
图 16.6 对树进行迭代 图 16.6 里定义的新例程是 > (map3 #'(lambda (&rest args) (princ args))
'(a b c d))
(A NIL B) (B A C) (C B D) (D C NIL)
NIL
和
> (map3 #'(lambda (c p n)
(princ c)
(if n (princ " | ")))
'(a b c d))
A | B | C | D
NIL
程序员经常会遇到上面的这类问题,但只要花些功夫,定义一些例程来处理它们,就能为后续工作节省不少时间。 16.4 生成页面 (Generating Pages)¶一本书可以有任意数量的大章,每个大章又有任意数量的小节,而每个小节又有任意数量的分节,整本书的结构呈现出一棵树的形状。 尽管网页使用的术语和书本不同,但多个网页同样可以被组织成树状。 本节要构建的是这样一个程序,它生成多个网页,这些网页带有以下结构: 第一页是一个目录,目录中的链接指向各个节点(section)页面。 每个节点包含一些指向项(item)的链接。 而一个项就是一个包含纯文本的页面。 除了页面本身的链接以外,根据页面在树状结构中的位置,每个页面都会带有前进、后退和向上的链接。 其中,前进和后退链接用于在同级(sibling)页面中进行导航。 举个例子,点击一个项页面中的前进链接时,如果这个项的同一个节点下还有下一个项,那么就跳到这个新项的页面里。 另一方面,向上链接将页面跳转到树形结构的上一层 —— 如果当前页面是项页面,那么返回到节点页面;如果当前页面是节点页面,那么返回到目录页面。 最后,还会有索引页面:这个页面包含一系列链接,按字母顺序排列所有项。 ![]() 图 16.7 网站的结构 图 16.7 展示了生成程序创建的页面所形成的链接结构。 (defparameter *sections* nil)
(defstruct item
id title text)
(defstruct section
id title items)
(defmacro defitem (id title text)
`(setf ,id
(make-item :id ',id
:title ,title
:text ,text)))
(defmacro defsection (id title &rest items)
`(setf ,id
(make-section :id ',id
:title ,title
:items (list ,@items))))
(defun defsite (&rest sections)
(setf *sections* sections))
图 16.8 定义一个网站 图 16.8 包含定义页面所需的数据结构。程序需要处理两类对象:项和节点。这两类对象的结构很相似,不过节点包含的是项的列表,而项包含的是文本块。 节点和项两类对象都带有 节点和项也同时带有 在节点里,项的排列顺序由传给 (defconstant contents "contents")
(defconstant index "index")
(defun gen-contents (&optional (sections *sections*))
(page contents contents
(with ol
(dolist (s sections)
(link-item (section-id s) (section-title s))
(brs 2))
(link-item index (string-capitalize index)))))
(defun gen-index (&optional (sections *sections*))
(page index index
(with ol
(dolist (i (all-items sections))
(link-item (item-id i) (item-title i))
(brs 2)))))
(defun all-items (sections)
(let ((is nil))
(dolist (s sections)
(dolist (i (section-items s))
(setf is (merge 'list (list i) is #'title<))))
is))
(defun title< (x y)
(string-lessp (item-title x) (item-title y)))
图 16.9 生成索引和目录 图 16.9 包含的函数用于生成索引和目录。
常量 函数 实际程序中的对比操作通常更复杂一些。举个例子,它们需要忽略无意义的句首词汇,比如 (defun gen-site ()
(map3 #'gen-section *sections*)
(gen-contents)
(gen-index))
(defun gen-section (sect <sect sect>)
(page (section-id sect) (section-title sect)
(with ol
(map3 #'(lambda (item <item item>)
(link-item (item-id item)
(item-title item))
(brs 2)
(gen-item sect item <item item>))
(section-items sect)))
(brs 3)
(gen-move-buttons (if <sect (section-id <sect))
contents
(if sect> (section-id sect>)))))
(defun gen-item (sect item <item item>)
(page (item-id item) (item-title item)
(princ (item-text item))
(brs 3)
(gen-move-buttons (if <item (item-id <item))
(section-id sect)
(if item> (item-id item>)))))
(defun gen-move-buttons (back up forward)
(if back (button back "Back"))
(if up (button up "Up"))
(if forward (button forward "Forward")))
图 16.10 生成网站、节点和项 图 16.10 包含其余的代码: 所有页面的集合包括目录、索引、各个节点以及各个项的页面。
目录和索引的生成由图 16.9 中的代码完成。
节点和项由分别由生成节点页面的 这两个函数的开头和结尾非常相似。
它们都接受一个对象、对象的左兄弟、对象的右兄弟作为参数;它们都从对象的 项所包含的内容完全由用户决定。 比如说,将 HTML 标签作为内容也是完全没问题的。 项的文本当然也可以由其他程序来生成。 图 16.11 演示了如何手工地定义一个微型网页。 在这个例子中,列出的项都是 Fortune 饼干公司新推出的产品。 (defitem des "Fortune Cookies: Dessert or Fraud?" "...")
(defitem case "The Case for Pessimism" "...")
(defsection position "Position Papers" des case)
(defitem luck "Distribution of Bad Luck" "...")
(defitem haz "Health Hazards of Optimism" "...")
(defsection abstract "Research Abstracts" luck haz)
(defsite position abstract)
图 16.11 一个微型网站 第十七章:示例:对象¶在本章里,我们将使用 Lisp 来自己实现面向对象语言。这样子的程序称为嵌入式语言 (embedded language)。嵌入一个面向对象语言到 Lisp 里是一个绝佳的例子。同時作为一个 Lisp 的典型用途,並演示了面向对象的抽象是如何多自然地在 Lisp 基本的抽象上构建出来。 17.1 继承 (Inheritance)¶11.10 小节解释过通用函数与消息传递的差别。 在消息传递模型里,
当然了,我们知道 CLOS 使用的是通用函数模型。但本章我们只对于写一个迷你的对象系统 (minimal object system)感兴趣,而不是一个可与 CLOS 匹敌的系统,所以我们将使用消息传递模型。 我们已经在 Lisp 里看过许多保存属性集合的方法。一种可能的方法是使用哈希表来代表对象,并将属性作为哈希表的条目保存。接著可以通过 (gethash 'color obj)
由于函数是数据对象,我们也可以将函数作为属性保存起来。这表示我们也可以有方法;要调用一个对象特定的方法,可以通过 (funcall (gethash 'move obj) obj 10)
我们可以在这个概念上,定义一个 Smalltalk 风格的消息传递语法, (defun tell (obj message &rest args)
(apply (gethash message obj) obj args))
所以想要一个对象 (tell obj 'move 10)
事实上,纯 Lisp 唯一缺少的原料是继承。我们可以通过定义一个递归版本的 (defun rget (prop obj)
(multiple-value-bind (val in) (gethash prop obj)
(if in
(values val in)
(let ((par (gethash :parent obj)))
(and par (rget prop par))))))
(defun tell (obj message &rest args)
(apply (rget message obj) obj args))
图 17.1:继承 让我们用这段代码,来试试本来的例子。我们创建两个对象,其中一个对象是另一个的子类: > (setf circle-class (make-hash-table)
our-circle (make-hash-table)
(gethash :parent our-circle) circle-class
(gethash 'radius our-circle) 2)
2
> (setf (gethash 'area circle-class)
#'(lambda (x)
(* pi (expt (rget 'radius x) 2))))
#<Interpreted-Function BF1EF6>
现在当我们询问 > (rget 'radius our-circle)
2
T
> (tell our-circle 'area)
12.566370614359173
在开始改善这个程序之前,值得停下来想想我们到底做了什么。仅使用 8 行代码,我们使纯的、旧的、无 CLOS 的 Lisp ,转变成一个面向对象语言。我们是怎么完成这项壮举的?应该用了某种秘诀,才会仅用了 8 行代码,就实现了面向对象编程。 的确有一个秘诀存在,但不是编程的奇技淫巧。这个秘诀是,Lisp 本来就是一个面向对象的语言了,甚至说,是种更通用的语言。我们需要做的事情,不过就是把本来就存在的抽象,再重新包装一下。 17.2 多重继承 (Multiple Inheritance)¶到目前为止我们只有单继承 ── 一个对象只可以有一个父类。但可以通过使 在只有单继承的情况下,当我们想要从对象取出某些属性,只需要递归地延著祖先的方向往上找。如果对象本身没有我们想要属性的有关信息,可以检视其父类,以此类推。有了多重继承后,我们仍想要执行同样的搜索,但这件简单的事,却被对象的祖先可形成一个图,而不再是简单的树给复杂化了。不能只使用深度优先来搜索这个图。有多个父类时,可以有如图 17.3 所示的层级存在: 如果我们想要实现普遍的继承概念,就不应该在检查其子孙前,先检查该对象。在这个情况下,适当的搜索顺序会是 我们可以通过利用优先级列表的优点,举例来说,一个爱国的无赖先是一个无赖,然后才是爱国者: > (setf scoundrel (make-hash-table)
patriot (make-hash-table)
patriotic-scoundrel (make-hash-table)
(gethash 'serves scoundrel) 'self
(gethash 'serves patriot) 'country
(gethash :parents patriotic-scoundrel)
(list scoundrel patriot))
(#<Hash-Table C41C7E> #<Hash-Table C41F0E>)
> (rget 'serves patriotic-scoundrel)
SELF
T
到目前为止,我们有一个强大的程序,但极其丑陋且低效。在一个 Lisp 程序生命周期的第二阶段,我们将这个初步框架提炼成有用的东西。 17.3 定义对象 (Defining Objects)¶第一个我们需要改善的是,写一个用来创建对象的函数。我们程序表示对象以及其父类的方式,不需要给用户知道。如果我们定义一个函数来创建对象,用户将能够一个步骤就创建出一个对象,并指定其父类。我们可以在创建一个对象的同时,顺道构造优先级列表,而不是在每次当我们需要找一个属性或方法时,才花费庞大代价来重新构造。 如果我们要维护优先级列表,而不是在要用的时候再构造它们,我们需要处理列表会过时的可能性。我们的策略会是用一个列表来保存所有存在的对象,而无论何时当某些父类被改动时,重新给所有受影响的对象生成优先级列表。这代价是相当昂贵的,但由于查询比重定义父类的可能性来得高许多,我们会省下许多时间。这个改变对我们的程序的灵活性没有任何影响;我们只是将花费从频繁的操作转到不频繁的操作。 图 17.4 包含了新的代码。 λ 全局的 用户现在不用调用 (defvar *objs* nil)
(defun parents (obj) (gethash :parents obj))
(defun (setf parents) (val obj)
(prog1 (setf (gethash :parents obj) val)
(make-precedence obj)))
(defun make-precedence (obj)
(setf (gethash :preclist obj) (precedence obj))
(dolist (x *objs*)
(if (member obj (gethash :preclist x))
(setf (gethash :preclist x) (precedence x)))))
(defun obj (&rest parents)
(let ((obj (make-hash-table)))
(push obj *objs*)
(setf (parents obj) parents)
obj))
(defun rget (prop obj)
(dolist (c (gethash :preclist obj))
(multiple-value-bind (val in) (gethash prop c)
(if in (return (values val in))))))
图 17.4:创建对象 17.4 函数式语法 (Functional Syntax)¶另一个可以改善的空间是消息调用的语法。 (tell (tell obj 'find-owner) 'find-owner)
我们可以使用图 17.5 所定义的 (defmacro defprop (name &optional meth?)
`(progn
(defun ,name (obj &rest args)
,(if meth?
`(run-methods obj ',name args)
`(rget ',name obj)))
(defun (setf ,name) (val obj)
(setf (gethash ',name obj) val))))
(defun run-methods (obj name args)
(let ((meth (rget name obj)))
(if meth
(apply meth obj args)
(error "No ~A method for ~A." name obj))))
图 17.5: 函数式语法 (defprop find-owner t)
我们就可以在函数调用里引用它,则我们的代码读起来将会再次回到 Lisp 本来那样: (find-owner (find-owner obj))
我们的前一个例子在某种程度上可读性变得更高了: > (progn
(setf scoundrel (obj)
patriot (obj)
patriotic-scoundrel (obj scoundrel patriot))
(defprop serves)
(setf (serves scoundrel) 'self
(serves patriot) 'country)
(serves patriotic-scoundrel))
SELF
T
17.5 定义方法 (Defining Methods)¶到目前为止,我们借由叙述如下的东西来定义一个方法: (defprop area t)
(setf circle-class (obj))
(setf (area circle-class)
#'(lambda (c) (* pi (expt (radius c) 2))))
(defmacro defmeth (name obj parms &rest body)
(let ((gobj (gensym)))
`(let ((,gobj ,obj))
(setf (gethash ',name ,gobj)
(labels ((next () (get-next ,gobj ',name)))
#'(lambda ,parms ,@body))))))
(defun get-next (obj name)
(some #'(lambda (x) (gethash name x))
(cdr (gethash :preclist obj))))
图 17.6 定义方法。 在一个方法里,我们可以通过给对象的 (setf grumpt-circle (obj circle-class))
(setf (area grumpt-circle)
#'(lambda (c)
(format t "How dare you stereotype me!~%")
(funcall (some #'(lambda (x) (gethash 'area x))
(cdr (gethash :preclist c)))
c)))
这里 图 17.6 的 (defmeth area circle-class (c)
(* pi (expt (radius c) 2)))
(defmeth area grumpy-circle (c)
(format t "How dare you stereotype me!~%")
(funcall (next) c))
顺道一提,注意 17.6 实例 (Instances)¶到目前为止,我们还没有将类别与实例做区别。我们使用了一个术语来表示两者,对象(object)。将所有的对象视为一体是优雅且灵活的,但这非常没效率。在许多面向对象应用里,继承图的底部会是复杂的。举例来说,模拟一个交通情况,我们可能有少于十个对象来表示车子的种类,但会有上百个对象来表示特定的车子。由于后者会全部共享少数的优先级列表,创建它们是浪费时间的,并且浪费空间来保存它们。 图 17.7 定义一个宏 (setf grumpy-circle (inst circle-class))
由于某些对象不再有优先级列表,函数 17.7 新的实现 (New Implementation)¶我们到目前为止所做的改善都是牺牲灵活性交换而来。在这个系统的开发后期,一个 Lisp 程序通常可以牺牲些许灵活性来获得好处,这里也不例外。目前为止我们使用哈希表来表示所有的对象。这给我们带来了超乎我们所需的灵活性,以及超乎我们所想的花费。在这个小节里,我们会重写我们的程序,用简单向量来表示对象。 (defun inst (parent)
(let ((obj (make-hash-table)))
(setf (gethash :parents obj) parent)
obj))
(defun rget (prop obj)
(let ((prec (gethash :preclist obj)))
(if prec
(dolist (c prec)
(multiple-value-bind (val in) (gethash prop c)
(if in (return (values val in)))))
(multiple-value-bind (val in) (gethash prop obj)
(if in
(values val in)
(rget prop (gethash :parents obj)))))))
(defun get-next (obj name)
(let ((prec (gethash :preclist obj)))
(if prec
(some #'(lambda (x) (gethash name x))
(cdr prec))
(get-next (gethash obj :parents) name))))
图 17.7: 定义实例 这个改变意味著放弃动态定义新属性的可能性。目前我们可通过引用任何对象,给它定义一个属性。现在当一个类别被创建时,我们会需要给出一个列表,列出该类有的新属性,而当实例被创建时,他们会恰好有他们所继承的属性。 在先前的实现里,类别与实例没有实际区别。一个实例只是一个恰好有一个父类的类别。如果我们改动一个实例的父类,它就变成了一个类别。在新的实现里,类别与实例有实际区别;它使得将实例转成类别不再可能。 在图 17.8-17.10 的代码是一个完整的新实现。图片 17.8 给创建类别与实例定义了新的操作符。类别与实例用向量来表示。表示类别与实例的向量的前三个元素包含程序自身要用到的信息,而图 17.8 的前三个宏是用来引用这些元素的: (defmacro parents (v) `(svref ,v 0))
(defmacro layout (v) `(the simple-vector (svref ,v 1)))
(defmacro preclist (v) `(svref ,v 2))
(defmacro class (&optional parents &rest props)
`(class-fn (list ,@parents) ',props))
(defun class-fn (parents props)
(let* ((all (union (inherit-props parents) props))
(obj (make-array (+ (length all) 3)
:initial-element :nil)))
(setf (parents obj) parents
(layout obj) (coerce all 'simple-vector)
(preclist obj) (precedence obj))
obj))
(defun inherit-props (classes)
(delete-duplicates
(mapcan #'(lambda (c)
(nconc (coerce (layout c) 'list)
(inherit-props (parents c))))
classes)))
(defun precedence (obj)
(labels ((traverse (x)
(cons x
(mapcan #'traverse (parents x)))))
(delete-duplicates (traverse obj))))
(defun inst (parent)
(let ((obj (copy-seq parent)))
(setf (parents obj) parent
(preclist obj) nil)
(fill obj :nil :start 3)
obj))
图 17.8: 向量实现:创建
因为这些操作符是宏,他们全都可以被
> (setf *print-array* nil
gemo-class (class nil area)
circle-class (class (geom-class) radius))
#<Simple-Vector T 5 C6205E>
这里我们创建了两个类别: > (coerce (layout circle-class) 'list)
(AREA RADIUS)
显示了五个字段里,最后两个的名称。 [2]
> (svref circle-class
(+ (position 'area (layout circle-class)) 3))
:NIL
稍后我们会定义存取函数来自动办到这件事。 最后,函数 > (setf our-circle (inst circle-class))
#<Simple-Vector T 5 C6464E>
比较 (declaim (inline lookup (setf lookup)))
(defun rget (prop obj next?)
(let ((prec (preclist obj)))
(if prec
(dolist (c (if next? (cdr prec) prec) :nil)
(let ((val (lookup prop c)))
(unless (eq val :nil) (return val))))
(let ((val (lookup prop obj)))
(if (eq val :nil)
(rget prop (parents obj) nil)
val)))))
(defun lookup (prop obj)
(let ((off (position prop (layout obj) :test #'eq)))
(if off (svref obj (+ off 3)) :nil)))
(defun (setf lookup) (val prop obj)
(let ((off (position prop (layout obj) :test #'eq)))
(if off
(setf (svref obj (+ off 3)) val)
(error "Can't set ~A of ~A." val obj))))
图 17.9: 向量实现:存取 现在我们可以创建所需的类别层级及实例,以及需要的函数来读写它们的属性。图 17.9 的第一个函数是
函数 > (lookup 'area circle-class)
:NIL
由于 (setf (lookup 'area circle-class)
#'(lambda (c)
(* pi (expt (rget 'radius c nil) 2))))
在这个程序里,和先前的版本一样,没有特别区别出方法与槽。一个“方法”只是一个字段,里面有着一个函数。这将很快会被一个更方便的前端所隐藏起来。 (declaim (inline run-methods))
(defmacro defprop (name &optional meth?)
`(progn
(defun ,name (obj &rest args)
,(if meth?
`(run-methods obj ',name args)
`(rget ',name obj nil)))
(defun (setf ,name) (val obj)
(setf (lookup ',name obj) val))))
(defun run-methods (obj name args)
(let ((meth (rget name obj nil)))
(if (not (eq meth :nil))
(apply meth obj args)
(error "No ~A method for ~A." name obj))))
(defmacro defmeth (name obj parms &rest body)
(let ((gobj (gensym)))
`(let ((,gobj ,obj))
(defprop ,name t)
(setf (lookup ',name ,gobj)
(labels ((next () (rget ,gobj ',name t)))
#'(lambda ,parms ,@body))))))
图 17.10: 向量实现:宏介面 图 17.10 包含了新的实现的最后部分。这个代码没有给程序加入任何威力,但使程序更容易使用。宏 > (defprop radius)
(SETF RADIUS)
> (radius our-circle)
:NIL
> (setf (radius our-circle) 2)
2
如果 最后,函数 现在我们可以达到先前方法定义所有的效果,但更加清晰: (defmeth area circle-class (c)
(* pi (expt (radius c) 2)))
注意我们可以直接调用 > (area our-circle)
12.566370614359173
17.8 分析 (Analysis)¶我们现在有了一个适合撰写实际面向对象程序的嵌入式语言。它很简单,但就大小来说相当强大。而在典型应用里,它也会是快速的。在一个典型的应用里,操作实例应比操作类别更常见。我们重新设计的重点在于如何使得操作实例的花费降低。 在我们的程序里,创建类别既慢且产生了许多垃圾。如果类别不是在速度为关键考量时创建,这还是可以接受的。会需要速度的是存取函数以及创建实例。这个程序里的没有做编译优化的存取函数大约与我们预期的一样快。 λ 而创建实例也是如此。且两个操作都没有用到构造 (consing)。除了用来表达实例的向量例外。会自然的以为这应该是动态地配置才对。但我们甚至可以避免动态配置实例,如果我们使用像是 13.4 节所提出的策略。 我们的嵌入式语言是 Lisp 编程的一个典型例子。只不过是一个嵌入式语言就可以是一个例子了。但 Lisp 的特性是它如何从一个小的、受限版本的程序,进化成一个强大但低效的版本,最终演化成快速但稍微受限的版本。 Lisp 恶名昭彰的缓慢不是 Lisp 本身导致(Lisp 编译器早在 1980 年代就可以产生出与 C 编译器一样快的代码),而是由于许多程序员在第二个阶段就放弃的事实。如同 Richard Gabriel 所写的,
这完全是一个真的论述,但也可以解读为赞扬或贬低 Lisp 的论点:
你的程序属于哪一种解读完全取决于你。但至少在开发初期,Lisp 使你有牺牲执行速度来换取时间的选择。 有一件我们示例程序没有做的很好的事是,它不是一个称职的 CLOS 模型(除了可能没有说明难以理解的 我们程序与 CLOS 不同的地方是,方法是属于某个对象的。这个方法的概念使它们与对第一个参数做派发的函数相同。而当我们使用函数式语法来调用方法时,这看起来就跟 Lisp 的函数一样。相反地,一个 CLOS 的通用函数,可以派发它的任何参数。一个通用函数的组件称为方法,而若你将它们定义成只对第一个参数特化,你可以制造出它们是某个类或实例的方法的错觉。但用面向对象编程的消息传递模型来思考 CLOS 最终只会使你困惑,因为 CLOS 凌驾在面向对象编程之上。 CLOS 的缺点之一是它太庞大了,并且 CLOS 费煞苦心的隐藏了面向对象编程,其实只不过是改写 Lisp 的这个事实。本章的例子至少阐明了这一点。如果我们满足于旧的消息传递模型,我们可以用一页多一点的代码来实现。面向对象编程不过是 Lisp 可以做的小事之一而已。更发人深省的问题是,Lisp 除此之外还能做些什么? 脚注
附录 A:调试¶这个附录演示了如何调试 Lisp 程序,并给出你可能会遇到的常见错误。 中断循环 (Breakloop)¶如果你要求 Lisp 做些它不能做的事,求值过程会被一个错误讯息中断,而你会发现你位于一个称为中断循环的地方。中断循环工作的方式取决于不同的实现,但通常它至少会显示三件事:一个错误信息,一组选项,以及一个特别的提示符。 在中断循环里,你也可以像在顶层那样给表达式求值。在中断循环里,你或许能够找出错误的起因,甚至是修正它,并继续你程序的求值过程。然而,在一个中断循环里,你想做的最常见的事是跳出去。多数的错误起因于打错字或是小疏忽,所以通常你只会想终止程序并返回顶层。在下面这个假定的实现里,我们输入 > (/ 1 0)
Error: Division by zero.
Options: :abort, :backtrace
>> :abort
>
在这些情况里,实际上的输入取决于实现。 当你在中断循环里,如果一个错误发生的话,你会到另一个中断循环。多数的 Lisp 会指出你是在第几层的中断循环,要嘛通过印出多个提示符,不然就是在提示符前印出数字: >> (/ 2 0)
Error: Division by zero.
Options: :abort, :backtrace, :previous
>>>
现在我们位于两层深的中断循环。此时我们可以选择回到前一个中断循环,或是直接返回顶层。 追踪与回溯 (Traces and Backtraces)¶当你的程序不如你预期的那样工作时,有时候第一件该解决的事情是,它在做什么?如果你输入 一个追踪通常会根据调用树来缩进。在一个做遍历的函数,像下面这个函数,它给一个树的每一个非空元素加上 1, (defun tree1+ (tr)
(cond ((null tr) nil)
((atom tr) (1+ tr))
(t (cons (treel+ (car tr))
(treel+ (cdr tr))))))
一个树的形状会因此反映出它被遍历时的数据结构: > (trace tree1+)
(tree1+)
> (tree1+ '((1 . 3) 5 . 7))
1 Enter TREE1+ ((1 . 3) 5 . 7)
2 Enter TREE1+ (1.3)
3 Enter TREE1+ 1
3 Exit TREE1+ 2
3 Enter TREE1+ 3
3 Exit TREE1+ 4
2 Exit TREE1+ (2 . 4)
2 Enter TREE1+ (5 . 7)
3 Enter TREE1+ 5
3 Exit TREE1+ 6
3 Enter TREE1+ 7
3 Exit TREE1+ 8
2 Exit TREE1+ (6 . 8)
1 Exit TREE1+ ((2 . 4) 6 . 8)
((2 . 4) 6 . 8)
要关掉 一个更灵活的追踪办法是在你的代码里插入诊断性的打印语句。如果已经知道结果了,这个经典的方法大概会与复杂的调适工具一样被使用数十次。这也是为什么可以互动地重定义函数式多么有用的原因。 一个回溯 (backtrace)是一个当前存在栈的调用的列表,当一个错误中止求值时,会由一个中断循环生成此列表。如果追踪像是”让我看看你在做什么”,一个回溯像是询问”我们是怎么到达这里的?” 在某方面上,追踪与回溯是互补的。一个追踪会显示在一个程序的调用树里,选定函数的调用。一个回溯会显示在一个程序部分的调用树里,所有函数的调用(路径为从顶层调用到发生错误的地方)。 在一个典型的实现里,我们可通过在中断循环里输入 > (tree1+ ' ( ( 1 . 3) 5 . A))
Error: A is not a valid argument to 1+.
Options: :abort, :backtrace
» :backtrace
(1+ A)
(TREE1+ A)
(TREE1+ (5 . A))
(TREE1+ ((1 . 3) 5 . A))
出现在回溯里的臭虫较容易被发现。你可以仅往回检查调用链,直到你找到第一个不该发生的事情。另一个函数式编程 (2.12 节)的好处是所有的臭虫都会在回溯里出现。在纯函数式代码里,每一个可能出错的调用,在错误发生时,一定会在栈出现。 一个回溯每个实现所提供的信息量都不同。某些实现会完整显示一个所有待调用的历史,并显示参数。其他实现可能仅显示调用历史。一般来说,追踪与回溯解释型的代码会得到较多的信息,这也是为什么你要在确定你的程序可以工作之后,再来编译。 传统上我们在解释器里调试代码,且只在工作的情况下才编译。但这个观点也是可以改变的:至少有两个 Common Lisp 实现没有包含解释器。 当什么事都没发生时 (When Noting Happens)¶不是所有的 bug 都会打断求值过程。另一个常见并可能更危险的情况是,当 Lisp 好像不鸟你一样。通常这是程序进入无穷循环的徵兆。 如果你怀疑你进入了无穷循环,解决方法是中止执行,并跳出中断循环。 如果循环是用迭代写成的代码,Lisp 会开心地执行到天荒地老。但若是用递归写成的代码(没有做尾递归优化),你最终会获得一个信息,信息说 Lisp 把栈的空间给用光了: > (defun blow-stack () (1+ (blow-stack)))
BLOW-STACK
> (blow-stack)
Error: Stack Overflow
在这两个情况里,如果你怀疑进入了无穷循环,解决办法是中断执行,并跳出由于中断所产生的中断循环。 有时候程序在处理一个非常庞大的问题时,就算没有进入无穷循环,也会把栈的空间用光。虽然这很少见。通常把栈空间用光是编程错误的徵兆。 递归函数最常见的错误是忘记了基本用例 (base case)。用英语来描述递归,通常会忽略基本用例。不严谨地说,我们可能说“obj 是列表的成员,如果它是列表的第一个元素,或是剩余列表的成员” 严格上来讲,应该添加一句“若列表为空,则 obj 不是列表的成员”。不然我们描述的就是个无穷递归了。 在 Common Lisp 里,如果给入 > (car nil)
NIL
> (cdr nil)
NIL
所以若我们在 (defun our-member (obj lst)
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst))))
要是我们找的对象不在列表里的话,则会陷入无穷循环。当我们到达列表底端而无所获时,递归调用会等价于: (our-member obj nil)
在正确的定义中(第十六页「译注: 2.7 节」),基本用例在此时会停止递归,并返回 如果一个无穷循环的起因不是那么直观,可能可以通过看看追踪或回溯来诊断出来。无穷循环有两种。简单发现的那种是依赖程序结构的那种。一个追踪或回溯会即刻演示出,我们的 比较难发现的那种,是因为数据结构有缺陷才发生的无穷循环。如果你无意中创建了环状结构(见 199页「12.3 节」,遍历结构的代码可能会掉入无穷循环里。这些 bug 很难发现,因为不在后面不会发生,看起来像没有错误的代码一样。最佳的解决办法是预防,如同 199 页所描述的:避免使用破坏性操作,直到程序已经正常工作,且你已准备好要调优代码来获得效率。 如果 Lisp 有不鸟你的倾向,也有可能是等待你完成输入什么。在多数系统里,按下回车是没有效果的,直到你输入了一个完整的表达式。这个方法的好事是它允许你输入多行的表达式。坏事是如果你无意中少了一个闭括号,或是一个闭引号,Lisp 会一直等你,直到你真正完成输入完整的表达式: > (format t "for example ~A~% 'this)
这里我们在控制字符串的最后忽略了闭引号。在此时按下回车是没用的,因为 Lisp 认为我们还在输入一个字符串。 在某些实现里,你可以回到上一行,并插入闭引号。在不允许你回到前行的系统,最佳办法通常是中断执行,并从中断循环回到顶层。 没有值或未绑定 (No Value/Unbound)¶一个你最常听到 Lisp 的抱怨是一个符号没有值或未绑定。数种不同的问题都用这种方式呈现。 局部变量,如 > (progn
(let ((x 10))
(format t "Here x = ~A. ~%" x))
(format t "But now it's gone...~%")
x)
Here x = 10.
But now it's gone...
Error: X has no value.
我们获得一个错误。当 Lisp 抱怨某些东西没有值或未绑定时,它的意思通常是你无意间引用了一个不存在的变量。因为没有叫做 一个类似的问题发生在我们无意间将函数引用成变量。举例来说: > defun foo (x) (+ x 1))
Error: DEFUN has no value
这在第一次发生时可能会感到疑惑: 有可能你真的忘记初始化某个全局变量。如果你没有给 意料之外的 Nil (Unexpected Nils)¶当函数抱怨传入 举例来说,返回一个月有多少天的函数有一个 bug;假设我们忘记十月份了: (defun month-length (mon)
(case mon
((jan mar may jul aug dec) 31)
((apr jun sept nov) 30)
(feb (if (leap-year) 29 28))))
如果有另一个函数,企图想计算出一个月当中有几个礼拜, (defun month-weeks (mon) (/ (month-length mon) 7.0))
则会发生下面的情形: > (month-weeks 'oct)
Error: NIL is not a valud argument to /.
问题发生的原因是因为 在这里最起码 bug 与 bug 的临床表现是挨著发生的。这样的 bug 在它们相距很远时很难找到。要避免这个可能性,某些 Lisp 方言让跑完 重新命名 (Renaming)¶在某些场合里(但不是全部场合),有一种特别狡猾的 bug ,起因于重新命名函数或变量,。举例来说,假设我们定义下列(低效的) 函数来找出双重嵌套列表的深度: (defun depth (x)
(if (atom x)
1
(1+ (apply #'max (mapcar #'depth x)))))
测试函数时,我们发现它给我们错误的答案(应该是 1): > (depth '((a)))
3
起初的 (defun nesting-depth (x)
(if (atom x)
0
(1+ (apply #'max (mapcar #'depth x)))))
当我们再测试上面的例子,它返回同样的结果: > (nesting-depth '((a)))
3
我们不是修好这个函数了吗?没错,但答案不是来自我们修好的代码。我们忘记也改掉递归调用中的名称。在递归用例里,我们的新函数仍调用先前的 作为选择性参数的关键字 (Keywords as Optional Parameters)¶若函数同时接受关键字与选择性参数,这通常是个错误,无心地提供了关键字作为选择性参数。举例来说,函数 (read-from-string string &optional eof-error eof-value
&key start end preserve-whitespace)
这样一个函数你需要依序提供值,给所有的选择性参数,再来才是关键字参数。如果你忘记了选择性参数,看看下面这个例子, > (read-from-string "abcd" :start 2)
ABCD
4
则 > (read-from-string "abcd" nil nil :start 2)
CD
4
错误声明 (Misdeclarations)¶第十三章解释了如何给变量及数据结构做类型声明。通过给变量做类型声明,你保证变量只会包含某种类型的值。当产生代码时,Lisp 编译器会依赖这个假定。举例来说,这个函数的两个参数都声明为 (defun df* (a b)
(declare (double-float a b))
(* a b))
因此编译器在产生代码时,被授权直接将浮点乘法直接硬连接 (hard-wire)到代码里。 如果调用 > (df* 2 3)
Error: Interrupt.
如果获得这样严重的错误,通常是由于数值不是先前声明的类型。 警告 (Warnings)¶有些时候 Lisp 会抱怨一下,但不会中断求值过程。许多这样的警告是错误的警钟。一种最常见的可能是由编译器所产生的,关于未宣告或未使用的变量。举例来说,在 66 页「译注: 6.4 节」, (map-int #'(lambda (x)
(declare (ignore x))
(random 100))
10)
附录 B:Lisp in Lisp¶这个附录包含了 58 个最常用的 Common Lisp 操作符。因为如此多的 Lisp 是(或可以)用 Lisp 所写成,而由于 Lisp 程序(或可以)相当精简,这是一种方便解释语言的方式。 这个练习也证明了,概念上 Common Lisp 不像看起来那样庞大。许多 Common Lisp 操作符是有用的函式库;要写出所有其它的东西,你所需要的操作符相当少。在这个附录的这些定义只需要:
这里给出的代码作为一种解释 Common Lisp 的方式,而不是实现它的方式。在实际的实现上,这些操作符可以更高效,也会做更多的错误检查。为了方便参找,这些操作符本身按字母顺序排列。如果你真的想要这样定义 Lisp,每个宏的定义需要在任何调用它们的代码之前。 (defun -abs (n)
(if (typep n 'complex)
(sqrt (+ (expt (realpart n) 2) (expt (imagpart n) 2)))
(if (< n 0) (- n) n)))
(defun -adjoin (obj lst &rest args)
(if (apply #'member obj lst args) lst (cons obj lst)))
(defmacro -and (&rest args)
(cond ((null args) t)
((cdr args) `(if ,(car args) (-and ,@(cdr args))))
(t (car args))))
(defun -append (&optional first &rest rest)
(if (null rest)
first
(nconc (copy-list first) (apply #'-append rest))))
(defun -atom (x) (not (consp x)))
(defun -butlast (lst &optional (n 1))
(nreverse (nthcdr n (reverse lst))))
(defun -cadr (x) (car (cdr x)))
(defmacro -case (arg &rest clauses)
(let ((g (gensym)))
`(let ((,g ,arg))
(cond ,@(mapcar #'(lambda (cl)
(let ((k (car cl)))
`(,(cond ((member k '(t otherwise))
t)
((consp k)
`(member ,g ',k))
(t `(eql ,g ',k)))
(progn ,@(cdr cl)))))
clauses)))))
(defun -cddr (x) (cdr (cdr x)))
(defun -complement (fn)
#'(lambda (&rest args) (not (apply fn args))))
(defmacro -cond (&rest args)
(if (null args)
nil
(let ((clause (car args)))
(if (cdr clause)
`(if ,(car clause)
(progn ,@(cdr clause))
(-cond ,@(cdr args)))
`(or ,(car clause)
(-cond ,@(cdr args)))))))
(defun -consp (x) (typep x 'cons))
(defun -constantly (x) #'(lambda (&rest args) x))
(defun -copy-list (lst)
(labels ((cl (x)
(if (atom x)
x
(cons (car x)
(cl (cdr x))))))
(cons (car lst)
(cl (cdr lst)))))
(defun -copy-tree (tr)
(if (atom tr)
tr
(cons (-copy-tree (car tr))
(-copy-tree (cdr tr)))))
(defmacro -defun (name parms &rest body)
(multiple-value-bind (dec doc bod) (analyze-body body)
`(progn
(setf (fdefinition ',name)
#'(lambda ,parms
,@dec
(block ,(if (atom name) name (second name))
,@bod))
(documentation ',name 'function)
,doc)
',name)))
(defun analyze-body (body &optional dec doc)
(let ((expr (car body)))
(cond ((and (consp expr) (eq (car expr) 'declare))
(analyze-body (cdr body) (cons expr dec) doc))
((and (stringp expr) (not doc) (cdr body))
(if dec
(values dec expr (cdr body))
(analyze-body (cdr body) dec expr)))
(t (values dec doc body)))))
这个定义不完全正确,参见 (defmacro -do (binds (test &rest result) &rest body)
(let ((fn (gensym)))
`(block nil
(labels ((,fn ,(mapcar #'car binds)
(cond (,test ,@result)
(t (tagbody ,@body)
(,fn ,@(mapcar #'third binds))))))
(,fn ,@(mapcar #'second binds))))))
(defmacro -dolist ((var lst &optional result) &rest body)
(let ((g (gensym)))
`(do ((,g ,lst (cdr ,g)))
((atom ,g) (let ((,var nil)) ,result))
(let ((,var (car ,g)))
,@body))))
(defun -eql (x y)
(typecase x
(character (and (typep y 'character) (char= x y)))
(number (and (eq (type-of x) (type-of y))
(= x y)))
(t (eq x y))))
(defun -evenp (x)
(typecase x
(integer (= 0 (mod x 2)))
(t (error "non-integer argument"))))
(defun -funcall (fn &rest args) (apply fn args))
(defun -identity (x) x)
这个定义不完全正确:表达式 (defmacro -let (parms &rest body)
`((lambda ,(mapcar #'(lambda (x)
(if (atom x) x (car x)))
parms)
,@body)
,@(mapcar #'(lambda (x)
(if (atom x) nil (cadr x)))
parms)))
(defun -list (&rest elts) (copy-list elts))
(defun -listp (x) (or (consp x) (null x)))
(defun -mapcan (fn &rest lsts)
(apply #'nconc (apply #'mapcar fn lsts)))
(defun -mapcar (fn &rest lsts)
(cond ((member nil lsts) nil)
((null (cdr lsts))
(let ((lst (car lsts)))
(cons (funcall fn (car lst))
(-mapcar fn (cdr lst)))))
(t
(cons (apply fn (-mapcar #'car lsts))
(apply #'-mapcar fn
(-mapcar #'cdr lsts))))))
(defun -member (x lst &key test test-not key)
(let ((fn (or test
(if test-not
(complement test-not))
#'eql)))
(member-if #'(lambda (y)
(funcall fn x y))
lst
:key key)))
(defun -member-if (fn lst &key (key #'identity))
(cond ((atom lst) nil)
((funcall fn (funcall key (car lst))) lst)
(t (-member-if fn (cdr lst) :key key))))
(defun -mod (n m)
(nth-value 1 (floor n m)))
(defun -nconc (&optional lst &rest rest)
(if rest
(let ((rest-conc (apply #'-nconc rest)))
(if (consp lst)
(progn (setf (cdr (last lst)) rest-conc)
lst)
rest-conc))
lst))
(defun -not (x) (eq x nil))
(defun -nreverse (seq)
(labels ((nrl (lst)
(let ((prev nil))
(do ()
((null lst) prev)
(psetf (cdr lst) prev
prev lst
lst (cdr lst)))))
(nrv (vec)
(let* ((len (length vec))
(ilimit (truncate (/ len 2))))
(do ((i 0 (1+ i))
(j (1- len) (1- j)))
((>= i ilimit) vec)
(rotatef (aref vec i) (aref vec j))))))
(if (typep seq 'vector)
(nrv seq)
(nrl seq))))
(defun -null (x) (eq x nil))
(defmacro -or (&optional first &rest rest)
(if (null rest)
first
(let ((g (gensym)))
`(let ((,g ,first))
(if ,g
,g
(-or ,@rest))))))
这两个 Common Lisp 没有,但这里有几的定义会需要用到。 (defun pair (lst)
(if (null lst)
nil
(cons (cons (car lst) (cadr lst))
(pair (cddr lst)))))
(defun -pairlis (keys vals &optional alist)
(unless (= (length keys) (length vals))
(error "mismatched lengths"))
(nconc (mapcar #'cons keys vals) alist))
(defmacro -pop (place)
(multiple-value-bind (vars forms var set access)
(get-setf-expansion place)
(let ((g (gensym)))
`(let* (,@(mapcar #'list vars forms)
(,g ,access)
(,(car var) (cdr ,g)))
(prog1 (car ,g)
,set)))))
(defmacro -prog1 (arg1 &rest args)
(let ((g (gensym)))
`(let ((,g ,arg1))
,@args
,g)))
(defmacro -prog2 (arg1 arg2 &rest args)
(let ((g (gensym)))
`(let ((,g (progn ,arg1 ,arg2)))
,@args
,g)))
(defmacro -progn (&rest args) `(let nil ,@args))
(defmacro -psetf (&rest args)
(unless (evenp (length args))
(error "odd number of arguments"))
(let* ((pairs (pair args))
(syms (mapcar #'(lambda (x) (gensym))
pairs)))
`(let ,(mapcar #'list
syms
(mapcar #'cdr pairs))
(setf ,@(mapcan #'list
(mapcar #'car pairs)
syms)))))
(defmacro -push (obj place)
(multiple-value-bind (vars forms var set access)
(get-setf-expansion place)
(let ((g (gensym)))
`(let* ((,g ,obj)
,@(mapcar #'list vars forms)
(,(car var) (cons ,g ,access)))
,set))))
(defun -rem (n m)
(nth-value 1 (truncate n m)))
(defmacro -rotatef (&rest args)
`(psetf ,@(mapcan #'list
args
(append (cdr args)
(list (car args))))))
(defun -second (x) (cadr x))
(defmacro -setf (&rest args)
(if (null args)
nil
`(setf2 ,@args)))
(defmacro setf2 (place val &rest args)
(multiple-value-bind (vars forms var set)
(get-setf-expansion place)
`(progn
(let* (,@(mapcar #'list vars forms)
(,(car var) ,val))
,set)
,@(if args `((setf2 ,@args)) nil))))
(defun -signum (n)
(if (zerop n) 0 (/ n (abs n))))
(defun -stringp (x) (typep x 'string))
(defun -tailp (x y)
(or (eql x y)
(and (consp y) (-tailp x (cdr y)))))
(defun -third (x) (car (cdr (cdr x))))
(defun -truncate (n &optional (d 1))
(if (> n 0) (floor n d) (ceiling n d)))
(defmacro -typecase (arg &rest clauses)
(let ((g (gensym)))
`(let ((,g ,arg))
(cond ,@(mapcar #'(lambda (cl)
`((typep ,g ',(car cl))
(progn ,@(cdr cl))))
clauses)))))
(defmacro -unless (arg &rest body)
`(if (not ,arg)
(progn ,@body)))
(defmacro -when (arg &rest body)
`(if ,arg (progn ,@body)))
(defun -1+ (x) (+ x 1))
(defun -1- (x) (- x 1))
(defun ->= (first &rest rest)
(or (null rest)
(and (or (> first (car rest)) (= first (car rest)))
(apply #'->= rest))))
附录 C:Common Lisp 的改变¶目前的 ANSI Common Lisp 与 1984 年由 Guy Steele 一书 Common Lisp: the Language 所定义的 Common Lisp 有着本质上的不同。同时也与 1990 年该书的第二版大不相同,虽然差别比较小。本附录总结了重大的改变。1990年之后的改变独自列在最后一节。 附录 D:语言参考手册¶备注¶本节既是备注亦作为参考文献。所有列于此的书籍与论文皆值得阅读。 译注: 备注后面跟随的数字即书中的页码 备注 viii (Notes viii)¶Steele, Guy L., Jr., Scott E. Fahlman, Richard P. Gabriel, David A. Moon, Daniel L. Weinreb , Daniel G. Bobrow, Linda G. DeMichiel, Sonya E. Keene, Gregor Kiczales, Crispin Perdue, Kent M. Pitman, Richard C. Waters, 以及 John L White。 Common Lisp: the Language, 2nd Edition. Digital Press, Bedford (MA), 1990. 备注 1 (Notes 1)¶McCarthy, John. Recursive Functions of Symbolic Expressions and their Computation by Machine, Part I. CACM, 3:4 (April 1960), pp. 184-195. McCarthy, John. History of Lisp. In Wexelblat, Richard L. (Ed.) Histroy of Programming Languages. Academic Press, New York, 1981, pp. 173-197. 备注 3 (Notes 3)¶Brooks, Frederick P. The Mythical Man-Month. Addison-Wesley, Reading (MA), 1975, p. 16. Rapid prototyping is not just a way to write programs faster or better. It is a way to write programs that otherwise might not get written at all. Even the most ambitious people shrink from big undertakings. It’s easier to start something if one can convince oneself (however speciously) that it won’t be too much work. That’s why so many big things have begun as small things. Rapid prototyping lets us start small. 备注 4 (Notes 4)¶同上, 第 i 页。 备注 5 (Notes 5)¶Murray, Peter and Linda. The Art of the Renaissance. Thames and Hudson, London, 1963, p.85. 备注 5-2 (Notes 5-2)¶Janson, W.J. History of Art, 3rd Edition. Abrams, New York, 1986, p. 374. The analogy applies, of course, only to paintings done on panels and later on canvases. Well-paintings continued to be done in fresco. Nor do I mean to suggest that painting styles were driven by technological change; the opposite seems more nearly true. 备注 12 (Notes 12)¶
备注 17 (Notes 17)¶对递归概念有困扰的读者,可以查阅下列的书籍: Touretzky, David S. Common Lisp: A Gentle Introduction to Symbolic Computation. Benjamin/Cummings, Redwood City (CA), 1990, Chapter 8. Friedman, Daniel P., and Matthias Felleisen. The Little Lisper. MIT Press, Cambridge, 1987. 譯註:這本書有再版,可在這裡找到。 备注 26 (Notes 26)¶In ANSI Common Lisp there is also a 备注 28 (Notes 28)¶Gabriel, Richard P. Lisp Good News, Bad News, How to Win Big AI Expert, June 1991, p.34. 备注 46 (Notes 46)¶Another thing to be aware of when using sort: it does not guarantee to preserve the order of elements judged equal by the comparison function. For example, if you sort 备注 61 (Notes 61)¶A lot has been said about the benefits of comments, and little or nothing about their cost. But they do have a cost. Good code, like good prose, comes from constant rewriting. To evolve, code must be malleable and compact. Interlinear comments make programs stiff and diffuse, and so inhibit the evolution of what they describe. 备注 62 (Notes 62)¶Though most implementations use the ASCII character set, the only ordering that Common Lisp guarantees for characters is as follows: the 26 lowercase letters are in alphabetically ascending order, as are the uppercase letters, and the digits from 0 to 9. 备注 76 (Notes 76)¶The standard way to implement a priority queue is to use a structure called a heap. See: Sedgewick, Robert. Algorithms. Addison-Wesley, Reading (MA), 1988. 备注 81 (Notes 81)¶The definition of progn sounds a lot like the evaluation rule for Common Lisp function calls (page 9). Though (defun our-progn (ftrest args)
(car (last args)))
This would be horribly inefficient, but functionally equivalent to the real 备注 84 (Notes 84)¶The analogy to a lambda expression breaks down if the variable names are symbols that have special meanings in a parameter list. For example, (let ((&key 1) (&optional 2)))
is correct, but the corresponding lambda expression ((lambda (ftkey ftoptional)) 1 2)
is not. The same problem arises if you try to define do in terms of 备注 89 (Notes 89)¶Steele, Guy L., Jr., and Richard P. Gabriel. The Evolution of Lisp. ACM SIGPLANNotices 28:3 (March 1993). The example in the quoted passage was translated from Scheme into Common Lisp. 备注 91 (Notes 91)¶To make the time look the way people expect, you would want to ensure that minutes and seconds are represented with two digits, as in: (defun get-time-string ()
(multiple-value-bind (s m h) (get-decoded-time)
(format nil "~A:~2,,,'0@A:~2,,,'O@A" h m s)))
备注 94 (Notes 94)¶In a letter of March 18 (old style) 1751, Chesterfield writes: “It was notorious, that the Julian Calendar was erroneous, and had overcharged the solar year with eleven days. Pope Gregory the Thirteenth corrected this error [in 1582]; his reformed calendar was immediately received by all the Catholic powers of Europe, and afterwards adopted by all the Protestant ones, except Russia, Sweden, and England. It was not, in my opinion, very honourable for England to remain in a gross and avowed error, especially in such company; the inconveniency of it was likewise felt by all those who had foreign correspondences, whether political or mercantile. I determined, therefore, to attempt the reformation; I consulted the best lawyers, and the most skillful astronomers, and we cooked up a bill for that purpose. But then my difficulty began; I was to bring in this bill, which was necessarily composed of law jargon and astronomical calculations, to both of which I am an utter stranger. However, it was absolutely necessary to make the House of Lords think that I knew something of the matter; and also to make them believe that they knew something of it themselves, which they do not. For my own part, I could just as soon have talked Celtic or Sclavonian to them, as astronomy, and they would have understood me full as well; so I resolved to do better than speak to the purpose, and to please instead of informing them. I gave them, therefore, only an historical account of calendars, from the Egyptian down to the Gregorian, amusing them now and then with little episodes; but I was particularly attentive to the choice of my words, to the harmony and roundness of my periods, to my elocution, to my action. This succeeded, and ever will succeed; they thought I informed them, because I pleased them; and many of them said I had made the whole very clear to them; when, God knows, I had not even attempted it.” See: Roberts, David (Ed.) Lord Chesterfield’s Letters. Oxford University Press, Oxford, 1992. 备注 95 (Notes 95)¶In Common Lisp, a universal time is an integer representing the number of seconds since the beginning of 1900. The functions (defun num->date (n)
(multiple-value-bind (ig no re d m y)
(decode-universal-time n)
(values d m y)))
(defun date->num (d m y)
(encode-universal-time 1 0 0 d m y))
(defun date+ (d m y n)
(num->date (+ (date->num d m y)
(* 60 60 24 n))))
Besides the range limit, this approach has the disadvantage that dates tend not to be fixnums. 备注 100 (Notes 100)¶Although a call to (defstruct marble
color)
The following function takes a list of marbles and returns their color, if they all have the same color, or n i l if they have different colors: (defun uniform-color (1st)
(let ((c (marble-color (car 1st))))
(dolist (m (cdr 1st))
(unless (eql (marble-color m) c)
(return nil)))
c))
Although (defun (setf uniform-color) (val 1st)
(dolist (m 1st)
(setf (marble-color m) val)))
we can say (setf (uniform-color *marbles*) 'red)
to make the color of each element of 备注 100-2 (Notes 100-2)¶In older Common Lisp implementations, you have to use (defun (setf primo) (val 1st) (setf (car 1st) val))
is equivalent to (defsetf primo set-primo)
(defun set-primo (1st val) (setf (car 1st) val))
备注 106 (Notes 106)¶C, for example, lets you pass a pointer to a function, but there’s less you can pass in a function (because C doesn’t have closures) and less the recipient can do with it (because C has no equivalent of apply). What’s more, you are in principle supposed to declare the type of the return value of the function you pass a pointer to. How, then, could you write 备注 109 (Notes 109)¶For many examples of the versatility of closures, see: Abelson, Harold, and Gerald Jay Sussman, with Julie Sussman. Structure and Interpretation of Computer Programs. MIT Press, Cambridge, 1985. 备注 109-2 (Notes 109-2)¶For more information about Dylan, see: Shalit, Andrew, with Kim Barrett, David Moon, Orca Starbuck, and Steve Strassmann. Dylan Interim Reference Manual. Apple Computer, 1994. At the time of printing this document was accessible from several sites, including http://www.harlequin.com and http://www.apple.com. Scheme is a very small, clean dialect of Lisp. It was invented by Guy L. Steele Jr. and Gerald J. Sussman in 1975, and is currently defined by: Clinger, William, and Jonathan A. Rees (Eds.) \(Revised^4\) Report on the Algorithmic Language Scheme. 1991. This report, and various implementations of Scheme, were at the time of printing available by anonymous FTP from swiss-ftp.ai.mit.edu:pub. There are two especially good textbooks that use Scheme—Structure and Interpretation (see preceding note) and: Springer, George and Daniel P. Friedman. Scheme and the Art of Programming. MIT Press, Cambridge, 1989. 备注 112 (Notes 112)¶The most horrible Lisp bugs may be those involving dynamic scope. Such errors almost never occur in Common Lisp, which has lexical scope by default. But since so many of the Lisps used as extension languages still have dynamic scope, practicing Lisp programmers should be aware of its perils. One bug that can arise with dynamic scope is similar in spirit to variable capture (page 166). You pass one function as an argument to another. The function passed as an argument refers to some variable. But within the function that calls it, the variable has a new and unexpected value. Suppose, for example, that we wrote a restricted version of mapcar as follows: (defun our-mapcar (fn x)
(if (null x)
nil (cons (funcall fn (car x))
(our-mapcar fn (cdr x)))))
Then suppose that we used this function in another function, (defun add-to-all (1st x)
(our-mapcar #'(lambda (num) (+ num x))
1st))
In Common Lisp this code works fine, but in a Lisp with dynamic scope it would generate an error. The function passed as an argument to 备注 123 (Notes 123)¶Newer implementations of Common Lisp include avariable 备注 125 (Notes 125)¶There are a number of ingenious algorithms for fast string-matching, but string-matching in text files is one of the cases where the brute-force approach is still reasonably fast. For more on string-matching algorithms, see: Sedgewick, Robert. Algorithms. Addison-Wesley, Reading (MA), 1988. 备注 141 (Notes 141)¶In 1984 CommonLisp, reduce did not take a (defun random-next (prev)
(let* ((choices (gethash prev *words*))
(i (random (let ((x 0))
(dolist (c choices)
(incf x (cdr c)))
x))))
(dolist (pair choices)
(if (minusp (decf i (cdr pair)))
(return (car pair))))))
备注 141-2 (Notes 141-2)¶In 1989, a program like Henley was used to simulate netnews postings by well-known flamers. The fake postings fooled a significant number of readers. Like all good hoaxes, this one had an underlying point. What did it say about the content of the original flames, or the attention with which they were read, that randomly generated postings could be mistaken for the real thing? One of the most valuable contributions of artificial intelligence research has been to teach us which tasks are really difficult. Some tasks turn out to be trivial, and some almost impossible. If artificial intelligence is concerned with the latter, the study of the former might be called artificial stupidity. A silly name, perhaps, but this field has real promise—it promises to yield programs that play a role like that of control experiments. Speaking with the appearance of meaning is one of the tasks that turn out to be surprisingly easy. People’s predisposition to find meaning is so strong that they tend to overshoot the mark. So if a speaker takes care to give his sentences a certain kind of superficial coherence, and his audience are sufficiently credulous, they will make sense of what he says. This fact is probably as old as human history. But now we can give examples of genuinely random text for comparison. And if our randomly generated productions are difficult to distinguish from the real thing, might that not set people to thinking? The program shown in Chapter 8 is about as simple as such a program could be, and that is already enough to generate “poetry” that many people (try it on your friends) will believe was written by a human being. With programs that work on the same principle as this one, but which model text as more than a simple stream of words, it will be possible to generate random text that has even more of the trappings of meaning. For a discussion of randomly generated poetry as a legitimate literary form, see: Low, Jackson M. Poetry, Chance, Silence, Etc. In Hall, Donald (Ed.) Claims for Poetry. University of Michigan Press, Ann Arbor, 1982. You bet. Thanks to the Online Book Initiative, ASCII versions of many classics are available online. At the time of printing, they could be obtained by anonymous FTP from ftp.std.com:obi. See also the Emacs Dissociated Press feature, which uses an equivalent algorithm to scramble a buffer. 备注 150 (Notes 150)¶下面这个函数会显示在一个给定实现中,16 个用来标示浮点表示法的限制的全局常量: (defun float-limits ()
(dolist (m '(most least))
(dolist (s '(positive negative))
(dolist (f '(short single double long))
(let ((n (intern (string-upcase
(format nil "~A-~A-~A-float"
m s f)))))
(format t "~30A ~A ~%" n (symbol-value n)))))))
备注 164 (Notes 164)¶快速排序演算法由霍尔于 1962 年发表,并被描述在 Knuth, D. E. Sorting and Searching. Addison-Wesley, Reading (MA), 1973.一书中。 备注 173 (Notes 173)¶Foderaro, John K. Introduction to the Special Lisp Section. CACM 34:9 (Setember 1991), p.27 备注 176 (Notes 176)¶关于 CLOS 更详细的信息,参考下列书目: Keene, Sonya E. Object Oriented Programming in Common Lisp , Addison-Wesley, Reading (MA), 1989 Kiczales, Gregor, Jim des Rivieres, and Daniel G. Bobrow. The Art of the Metaobject Protocol MIT Press, Cambridge, 1991 备注 178 (Notes 178)¶让我们再回放刚刚的句子一次:我们甚至不需要看程序中其他的代码一眼,就可以完成种种的改动。这个想法或许对某些读者听起来担忧地熟悉。这是写出面条式代码的食谱。 面向对象模型使得通过一点一点的来构造程序变得简单。但这通常意味著,在实践上它提供了一种有结构的方法来写出面条式代码。这不一定是坏事,但也不会是好事。 很多现实世界中的代码是面条式代码,这也许不能很快改变。针对那些终将成为面条式代码的程序来说,面向对象模型是好的:它们最起码会是有结构的面条。但针对那些也许可以避免误入崎途的程序来说,面向对象抽象只是更加危险的,而不是有用的。 备注 183 (Notes 183)¶When an instance would inherit a slot with the same name from several of its superclasses, the instance inherits a single slot that combines the properties of the slots in the superclasses. The way combination is done varies from property to property:
备注 191 (Notes 191)¶You can avoid explicitly uninterning the names of slots that you want to be encapsulated by using uninterned symbols as the names to start with: (progn
(defclass counter () ((#1=#:state :initform 0)))
(defmethod increment ((c counter))
(incf (slot-value c '#1#)))
(defmethod clear ((c counter))
(setf (slot-value c '#1#) 0)))
The (defvar *symtab* (make-hash-table :test #'equal))
(defun pseudo-intern (name)
(or (gethash name *symtab*)
(setf (gethash name *symtab*) (gensym))))
(set-dispatch-macro-character #\# #\[
#'(lambda (stream char1 char2)
(do ((acc nil (cons char acc))
(char (read-char stream) (read-char stream)))
((eql char #\]) (pseudo-intern acc)))))
Then it would be possible to say just: (defclass counter () ((#[state] :initform 0)))
(defmethod increment ((c counter))
(incf (slot-value c '#[state])))
(defmethod clear ((c counter))
(setf (slot-value c '#[state]) 0))
备注 204 (Notes 204)¶下面这个宏将新元素推入二叉搜索树: (defmacro bst-push (obj bst <)
(multiple-value-bind (vars forms var set access)
(get-setf-expansion bst)
(let ((g (gensym)))
`(let* ((,g ,obj)
,@(mapcar #'list vars forms)
(,(car var) (bst-insert! ,g ,access ,<)))
,set))))
备注 213 (Notes 213)¶Knuth, Donald E. Structured Programming with goto Statements. Computing Surveys , 6:4 (December 1974), pp. 261-301 备注 214 (Notes 214)¶Knuth, Donald E. Computer Programming as an Art In ACM Turing Award Lectures: The First Twenty Years. ACM Press, 1987 This paper and the preceding one are reprinted in: Knuth, Donald E. Literate Programming. CSLI Lecture Notes #27, Stanford University Center for the Study of Language and Information, Palo Alto, 1992. 备注 216 (Notes 216)¶Steele, Guy L., Jr. Debunking the “Expensive Procedure Call” Myth or, Procedural Call Implementations Considered Harmful or, LAMBDA: The Ultimate GOTO. Proceedings of the National Conference of the ACM, 1977, p. 157. Tail-recursion optimization should mean that the compiler will generate the same code for a tail-recursive function as it would for the equivalent 备注 217 (Notes 217)¶For some examples of calls to disassemble on various processors, see: Norvig, Peter. Paradigms ofArtificial Intelligence Programming: Case Studies in Common Lisp. Morgan Kaufmann, San Mateo (CA), 1992. 备注 218 (Notes 218)¶A lot of the increased popularity of object-oriented programming is more specifically the increased popularity of C++, and this in turn has a lot to do with typing. C++ gives you something that seems like a miracle in the conceptual world of C: the ability to define operators that work for different types of arguments. But you don’t need an object-oriented language to do this—all you need is run-time typing. And indeed, if you look at the way people use C++, the class hierarchies tend to be flat. C++ has become so popular not because people need to write programs in terms of classes and methods, but because people need a way around the restrictions imposed by C’s approach to typing. 备注 219 (Notes 219)¶Macros can make declarations easier. The following macro expects a type name and an expression (probably numeric), and expands the expression so that all arguments, and all intermediate results, are declared to be of that type. If you wanted to ensure that an expression e was evaluated using only fixnum arithmetic, you could say (defmacro with-type (type expr)
`(the ,type ,(if (atom expr)
expr
(expand-call type (binarize expr)))))
(defun expand-call (type expr)
`(,(car expr) ,@(mapcar #'(lambda (a)
`(with-type ,type ,a))
(cdr expr))))
(defun binarize (expr)
(if (and (nthcdr 3 expr)
(member (car expr) '(+ - * /)))
(destructuring-bind (op a1 a2 . rest) expr
(binarize `(,op (,op ,a1 ,a2) ,@rest)))
expr))
The call to binarize ensures that no arithmetic operator is called with more than two arguments. As the Lucid reference manual points out, a call like (the fixnum (+ (the fixnum a)
(the fixnum b)
(the fixnum c)))
still cannot be compiled into fixnum additions, because the intermediate results (e.g. a + b) might not be fixnums. Using (defun poly (a b x)
(with-type fixnum (+ (* a (expt x 2)) (* b x))))
If you wanted to do a lot of fixnum arithmetic, you might even want to define a read-macro that would expand into a 备注 224 (Notes 224)¶在许多 Unix 系统里, 备注 226 (Notes 229)¶T is a dialect of Scheme with many useful additions, including support for pools. For more on T, see: Rees, Jonathan A., Norman I. Adams, and James R. Meehan. The T Manual, 5th Edition. Yale University Computer Science Department, New Haven, 1988. The T manual, and T itself, were at the time of printing available by anonymous FTP from hing.lcs.mit.edu:pub/t3.1 . 备注 229 (Notes 229)¶The difference between specifications and programs is a difference in degree, not a difference in kind. Once we realize this, it seems strange to require that one write specifications for a program before beginning to implement it. If the program has to be written in a low-level language, then it would be reasonable to require that it be described in high-level terms first. But as the programming language becomes more abstract, the need for specifications begins to evaporate. Or rather, the implementation and the specifications can become the same thing. If the high-level program is going to be re-implemented in a lower-level language, it starts to look even more like specifications. What Section 13.7 is saying, in other words, is that the specifications for C programs could be written in Lisp. 备注 230 (Notes 230)¶Benvenuto Cellini’s story of the casting of his Perseus is probably the most famous (and the funniest) account of traditional bronze-casting: Cellini, Benvenuto. Autobiography. Translated by George Bull, Penguin Books, Harmondsworth, 1956. 备注 239 (Notes 239)¶Even experienced Lisp hackers find packages confusing. Is it because packages are gross, or because we are not used to thinking about what happens at read-time? There is a similar kind of uncertainty about def macro, and there it does seem that the difficulty is in the mind of the beholder. A good deal of work has gone into finding a more abstract alternative to def macro. But def macro is only gross if you approach it with the preconception (common enough) that defining a macro is like defining a function. Then it seems shocking that you suddenly have to worry about variable capture. When you think of macros as what they are, transformations on source code, then dealing with variable capture is no more of a problem than dealing with division by zero at run-time. So perhaps packages will turn out to be a reasonable way of providing modularity. It is prima facie evidence on their side that they resemble the techniques that programmers naturally use in the absence of a formal module system. 备注 242 (Notes 242)¶It might be argued that 备注 248 (Notes 248)¶关于更深入讲述逻辑推论的资料,参见:Stuart Russell 及 Peter Norvig 所著的 Artificial Intelligence: A Modern Approach。 备注 273 (Notes 273)¶Because the program in Chapter 17 takes advantage of the possibility of having a (proclaim '(inline lookup set-lookup))
(defsetf lookup set-lookup)
(defun set-lookup (prop obj val)
(let ((off (position prop (layout obj) :test #'eq)))
(if off
(setf (svref obj (+ off 3)) val)
(error "Can't set ~A of ~A." val obj))))
(defmacro defprop (name &optioanl meth?)
`(progn
(defun ,name (obj &rest args)
,(if meth?
`(run-methods obj ',name args)
`(rget ',name obj nil)))
(defsetf ,name (obj) (val)
`(setf (lookip ',',name ,obj) ,val))))
备注 276 (Notes 276)¶If (defmacro defmeth (name obj parms &rest body)
(let ((gobj (gensym)))
`(let ((,gobj ,obj))
(setf (gethash ',name ,gobj)
#'(lambda ,parms
(labels ((next ()
(funcall (get-next ,gobj ',name)
,@parms)))
,@body))))))
then it would be possible to invoke the next method simply by calling (defmeth area grumpy-circle (c)
(format t "How dare you stereotype me!""/,")
(next))
备注 284 (Notes 284)¶For really fast access to slots we would use the following macro: (defmacro with-slotref ((name prop class) &rest body)
(let ((g (gensym)))
`(let ((,g (+ 3 (position ,prop (layout ,class)
:test #'eq))))
(macrolet ((,name (obj) `(svref ,obj ,',g)))
,@body))))
It defines a local macro that refers directly to the vector element corresponding to a slot. If in some segment of code you wanted to refer to the same slot in many instances of the same class, with this macro the slot references would be straight For example, if the balloon class is defined as follows, (setf balloon-class (class nil size))
then this function pops (in the old sense) a list of ballons: (defun popem (ballons)
(with-slotref (bsize 'size balloon-class)
(dolist (b ballons)
(setf (bsize b) 0))))
备注 284-2 (Notes 284-2)¶Gabriel, Richard P. Lisp Good News, Bad News, How to Win Big AI Expert, June 1991, p.35. 早在 1973 年, Richard Fateman 已经能证明在 PDP-10 主机上, MacLisp 编译器比制造商的 FORTRAN 编译器,产生出更快速的代码。 备注 399 (Notes 399)¶It’s easiest to understand backquote if we suppose that backquote and comma are like quote, and that (defun eval2 (expr)
(case (and (consp expr) (car expr))
(comma (error "unmatched comma"))
(bq (eval-bq (second expr) 1))
(t (eval expr))))
(defun eval-bq (expr n)
(cond ((atom expr)
expr)
((eql (car expr) 'comma)
(if (= n 1)
(eval2 (second expr))
(list 'comma (eval-bq (second expr)
(1- n)))))
((eql (car expr) 'bq)
(list 'bq (eval-bq (second expr) (1+ n))))
(t
(cons (eval-bq (car expr) n)
(eval-bq (cdr expr) n)))))
In > (setf x 'a a 1 y 'b b 2)
2
> (eval2 '(bq (bq (w (comma x) (comma (comma y))))))
(BQ (W (COMMA X) (COMMA B)))
> (eval2 *)
(W A 2)
At some point a particularly remarkable molecule was formed by accident. We will call it the Replicator. It may not necessarily have been the biggest or the most complex molecule around, but it had the extraordinary property of being able to create copies of itself. Richard Dawkins The Selfish Gene We shall first define a class of symbolic expressions in terms of ordered pairs and lists. Then we shall define five elementary functions and predicates, and build from them by composition, conditional expressions, and recursive definitions an extensive class of functions of which we shall give a number of examples. We shall then show how these functions themselves can be expressed as symbolic expressions, and we shall define a universal function apply that allows us to compute from the expression for a given function its value for given arguments. John McCarthy Recursive Functions of Symbolic Expressions and their Computation by Machine, Part I |
P.Graham “ANSI Common LISP” Answer for Practice ── by SHIDO, Takafumi (takafumi@shido.info)
使用 huangz1990 所開發的 der 樣式。