V 0.90git
寫做本文的緣起:我也是一名 Common Lisp 的初學者,在對照着各類教程學習 Common Lisp 的過程當中,發現有很多細節都須要本身去摸索,好比對開發環境的進一步配置(推薦使用開發環境--LispBox 做爲一個一鍵式開發環境,大大下降了許多不熟悉 Emacs 的初學者的學習和使用門檻,不過遺憾的是它已經中止更新了,如今 LispBox 中各軟件的版本偏低,若是想要使用最新版本的 Common Lisp 實現,就須要本身去動手配置了),包括更新版本、支持中文符號名稱、自定義函數名始終高亮顯示等等,諸如此類不少細節,都須要本身摸索、嘗試和驗證,這個過程不可避免會花費一些時間。數組
我以爲只要有一我的經歷過這種摸索就能夠了,其餘人徹底能夠借鑑他現成的經驗,不然每一個人都去作重複的摸索,是一種至關大的浪費,因此就不揣冒昧,把本身學習過程當中的一些經驗和體會記錄下來,但願能爲其餘 Common Lisp 初學者節省一些時間。markdown
學習任何知識,都不能僅僅把它們當作知識,更重要的是要把它們在實際編程實踐中應用起來,持有這樣的學習觀念纔不至於讓你變成學究式的活字典,對於程序員來講這一點尤爲重要,你學習的任何語言知識,必定要在實際的程序編寫過程當中不斷練習、不停實踐,紙上得來終覺淺,絕知此事須躬行。網絡
寫做本文的目標是但願能爲 Common Lisp 初學者提供一份簡單易懂、容易上手、學習結合(學爲學,習爲實踐)的初學者教程。
【說明】:Lisp 家族至關龐大, Common Lisp 是其中的一個分支,具體的分類我就不在這裏贅述了,建議初學者能夠到 wiki 百科去了解,固然,若是你和我同樣,也在看冰河(田春)翻譯的《實用 Common Lisp 編程》這本書,那麼直接閱讀書中的前面章節也能夠對此有一個大體的瞭解。
我一貫認爲,學習任何知識體系都要遵循從易到難、從簡單到複雜的規律,就像 Lisp 的迭代式開發同樣,最初出如今程序員手中的版本是一個很不完善,但實現了基本核心功能的原型系統,而後再通過反覆的迭代開發來逐步完善,直到把粗糙的原型系統變成可用的工程系統。
學習一門複雜艱深、體系龐雜的程序語言--Common Lisp 一樣要遵循這個規律,這份教程也儘可能遵循這個規律來編寫,也就是開始時不會涉及太深刻的概念,只會提到一些基本概念,而這些基本概念也會以很直觀易懂的描述方式來表述,不會用到任何可能令初學者疑惑難解的術語,目的就是讓初學者對 Common Lisp 程序迅速創建一種感性認識跟理解,依據這些知識能夠迅速讀懂其餘開源做者寫的 Common Lisp 程序。
因此,本文的表述可能不是那麼嚴謹,好比本文會有這樣的表述:
「Lisp 程序由 S-表達式(符號表達式)組成,S-表達式是列表或者單個原子」 「列表是由 0 個或者更多的原子或者內部列表組成,原子或者列表之間由空格分隔開,並由括號括起來。 列表能夠是空的」
初學者看到這個表述就會對 Lisp 的列表創建一種初步直觀的印象,能夠據此識別程序中使用的列表,也能夠合法地構造出本身使用的列表,這樣初學者就能夠很迅速地入門了。
我不會在這裏說什麼
「列表本質上是由 cons(點對)構成的,點對錶示的列表是這個樣子(OR NULL CONS),點對不只能夠構成列表,還能夠構成樹、集合和查詢表」
雖說這種表述更確切,可是我以爲這種表述明顯會讓初學者感到複雜和困惑,這些內容應該放在初學者入門以後繼續深刻學習的過程當中逐步去了解的。
我發現以前看過的兩本教程在開始章節部分都不約而同地採用了很是直觀易懂的描述方式來說解列表,而把 點對cons 的講解放到了後續章節,看來你們都採起了一樣的講解策略。
所以,後續就再也不一一詳細解釋了,本文提到的所謂的 Lisp 基本概念都是針對初學者的入門階段的,等初學者真正入了門,再進一步深刻學習時,同時初學者也對 Common Lisp 的實現機制有了更深刻的瞭解時,天然會發如今這裏瞭解到的基本概念會有更底層、更確切的表述,那就用那些更確切的表述來更新你腦海中這些入門階段學到的直觀易懂的基本概念吧。
固然,爲了不沒必要要的誤解,我也會適當加一些說明,好比 Emacs Lisp 和 Common Lisp 的讀取求值機制不太同樣,Emacs Lisp 使用 Lisp 解釋器進行讀取和求值;而 Common Lisp 則使用 R-E-P-L 機制,要分爲讀取器(R)和求值器(E),讀取器處理的是 S-表達式,而求值器處理的則是通過讀取器處理輸出的一些特殊的 S-表達式:Lisp形式--form。
正如《實用 Common Lisp 編程》的做者所說 「難道在肯定一門語言真正有用以前就要先把它全部的細節都學完嗎?」,尤爲在面對 Common Lisp 這樣一門體系異常龐大的語言時,初學者在開始階段不可能也不必深究它全部的細節,先學一點最簡單的基礎知識---而憑藉這些基礎知識又足夠支撐你去寫一些最簡單的程序,而後讓你的簡單程序跑起來,這就是一個很好的開始了。
本文適用的讀者羣體就是 Common Lisp 的初學者,他們歷來沒有用 Lisp 寫過程序,對於 Lisp 的分類一無所知,也不清楚用什麼開發工具來運行 Common Lisp 程序,可是突然對 Common Lisp 產生了興趣,想學習一下,大體來講就是那些對於 Lisp 的認知停留在:「Lisp 是一門專門用於 AI 的學術性程序語言,不適合用來作商業開發」 這個程度的讀者----這也是我在看《黑客與畫家》以前對 Lisp 的認識。
我推薦的參考書,就是建議在學習過程當中備在案頭,能夠隨時查閱那種:
初學者階段:
《實用 Common Lisp 編程》 --比較適合初學者的常備工具書,不只有對 Common Lisp 的精彩講解,更有很是實用、貼切的例程進行參考
《ANSI Common Lisp》中文版 --基本讀物,能夠做爲額外的參考補充
《GNU Emacs Lisp 編程入門》 --此書專門講 Emacs Lisp ,和 Common Lisp 具體細節不太同樣,不過建議能對照着看看,會有意想不到的收穫,尤爲是其中的一些基本概念很接近;
《On Lisp 中文版》 --此書屬於擴展閱讀,有大量的代碼實例,重點放在傳授一種編程思想,主要探討 Common Lisp 的函數和宏,建議讀冰河翻譯的中文版,由於糾正了原文的一些代碼錯誤,初學者在瞭解一些基本概念以後就能夠看懂這個了,推薦
[《Google Common Lisp 風格》] 1 --該文檔涉及的範圍比較廣,建議先大體瀏覽,學到哪裏再細看哪裏的相關章節
迭代式學習:本文嘗試使用一種名爲迭代式學習的方式進行內容,也就是說按照從前到後的寫做順序,最前面出現的都是很是基本的知識和操做,讀者能夠邊閱讀、邊理解、邊實踐,能夠迅速從最簡單的部分入手,而後再以這部分比較簡單的知識爲基礎,不斷展開新的稍微難一點的內容,這部分的學習內容一樣須要遵循 邊閱讀、邊理解、邊實踐 的原則,等把這部分難度有所提高的內容掌握後,就到了更難一點的內容,繼續按照 邊閱讀、邊理解、邊實踐 的原則進行:
初級難度==》邊閱讀、邊理解、邊實踐 二級難度==》邊閱讀、邊理解、邊實踐 三級難度==》邊閱讀、邊理解、邊實踐 . . . . . . 超級難度==》邊閱讀、邊理解、邊實踐
看到這裏,有些對 Lisp 略有所知的朋友想必明白了,這不就是 Lisp 的迭代開發模式 Read-Eval-Print-Loop :REPL 的變形嗎?哈,恭喜你看穿了,就是這樣,通過一段時間使用 REPL 迭代方式的 Lisp 程序寫做,我發現這種探索性的漸進式迭代開發方式很是適合用來從無到有、從簡單到複雜構建一個全新的系統。
一種全新的知識體系也是這樣一個未知的須要漸進探索的大系統,人類的認知過程應該遵循這種從易到難、知行合一(理論+實踐)的漸進循環方式,這樣每一個學習階段你都能感受到進步,全部的反饋都是正面的,它既爲你帶來成就感,又能激勵你接受難度漸增的挑戰而期待更多的成就感,這種認知方式不會由於難度過高讓你產生挫折感,進而喪失繼續學習的興趣,正所謂:學而時習之,不亦樂乎(個人理解是:學習了理論知識而後去實踐中應用它,是多麼有趣啊!)。
對於初學者而言,必定要多看、多想、多試,千萬不要怕出錯,事實上,如今錯誤就是下一輪迭代的起點,從另外一個角度來講:若是你能把錯誤提示信息裏的各種錯誤所有都嘗試一遍,那你對這門語言也掌握得差很少了!
因此,初學者要要敢於思考、敢於嘗試、敢於犯錯!
【小提示】:
用一個簡單的文本文件把每次出錯的信息記錄下來,後面若是解決了就把解決方法也記錄一下,養成這種學習習慣,你會受益不淺。
在這裏,對於初學者而言, Common Lisp 體系通過多年的發展,就是這樣一種全新而複雜的知識體系,有不少內容相互關聯,我的自學起來難如下手,想要掌握這個知識體系,最好的辦法就是 REPL 迭代式學習,固然,這裏的難度迭代式教程寫做也是我我的的一種思考和探索,目前尚未獲得什麼實際驗證,是否可行還不必定,不過咱們能夠一塊兒在這裏嘗試一下,反正也沒什麼損失。:)
學習任何一門程序語言都不該該僅僅停留在理論階段,更重要的是實踐,有些概念可能看半天文字講解都不得要領,可是一寫成代碼,到計算機上跑一遍就比什麼講解都清楚了。
所以對於一門語言的初學者而言,動手實踐是很是重要的,可是不幸的事實是:配置開發環境是絕大多數初學者不得不面臨的一個難題,要麼是須要本身配置編譯參數、本身編譯版本,要麼是須要定製各類配置文件,並且經常會有一些莫名其妙的錯誤提示,讓初學者的第一個程序夭折,說老實話,這些知識點在初學者入門以後根本不算什麼,都是常識,可是在沒有入門以前,那就是天大的障礙,如今網絡比較發達,不少相似問題均可以上網搜索,之前網絡沒這麼發達的時候,初學者遇到這種問題那真是痛苦…
因此若是能有一個一鍵式的開發環境那是多麼幸福的事情,這樣初學者就能夠迅速進入狀態,避免無關的干擾,快速上手!
在這裏,Lisp 初學者們有福了,有一個很是簡單的一鍵式開發環境 LispBox 等着你們使用(雖然目前 LispBox 已經中止更新,不過託開源之福,咱們能夠本身更新版本),這個開發環境在各類主流平臺都提供了對應的版本,目前支持:
MS-Windows Mac OSX Linux
我使用了 MS-Windows 和 Mac OSX 下的版本,就目前使用狀況來看,仍是比較滿意的,所以我會強烈推薦初學者使用這個開發環境,它不須要你作任何配置,把壓縮包下載回來,解壓後直接雙擊可執行文件就能夠運行,沒有任何障礙。
LispBox 下載地址:
http://gigamonkeys.com/lispbox/
這個地址是《實用 Common Lisp 編程》的做者提供的,包括了寫給即將學習 Lisp 的新手們的一段話,你們能夠看看。
http://common-lisp.net/project/lispbox/
這裏是 LispBox 的正式下載地址
LispBox 其實是把 Emacs、Slime、Clozure CL 以及 QuickLisp 集成到一塊兒,關於 LispBox 更詳細具體的介紹能夠參考我之前寫的文章: 就再也不這裏重複了。
等初學者對 LispBox 熟悉一些後,就能夠本身修改配置來使用其餘 Common Lisp 實現了,好比加入 SBCL
使用 LispBox 作開發環境就至關於選擇了 Emacs 做爲編輯器、選擇 Slime 做爲交互界面,那麼必定要熟悉 Emacs 和 Slime 的各類快捷鍵,這不只會讓你的學習開發過程事半功倍,更讓你有一種高效率、不間斷鍵盤做業的享受。
建議參考讀物:
《GNU Emacs Lisp 編程入門》 -- 讓你瞭解 Emacs 工做的機制,明白那些插件是怎麼工做的
[《Slime 用戶手冊》] 2 -- 建議看帝歸翻譯的中文版,省時省力,全面介紹了 Slime 的快捷鍵
開發環境啓動後會進入一個 REPL 界面,咱們能夠直接在 CL-USER> 後面輸入 Lisp 代碼,而後敲擊回車運行代碼
; SLIME 2012-11-12 CL-USER>
第一個程序就沿用傳統,向世界打個招呼吧:
CL-USER> (print "hello,world!") "hello,world!" "hello,world!" CL-USER> (format t "你好, 世界!") 你好, 世界! NIL CL-USER>
這裏其實用了兩個函數,一個是 print 函數,一個是 format 函數,都是輸出內容到屏幕。
不過在 Common Lisp 中更經常使用 format 函數來輸出到屏幕多一些,能夠把它跟 C 語言的 printf 函數對照着來看,注意一下 format 中的那個參數 「t」,表明的是標準輸出流:*standard-output* ,也就是說若是在 t 的位置換一個參數,咱們也能夠把這段問候語發送到任何一個指定的輸出流上。
(format t "你好, 世界!")
這個結構就是一個列表,用括號包圍,裏面共有 3 個元素,這些元素用空格分隔,不過雙引號裏的空格做爲字符串內容處理,不起分隔做用,能夠很明顯地看出,format 屬於比較特殊的符號,它就是一個函數名,後面的兩個元素都是它的參數。
OK,是否是很簡單,就跟 Lisp 世界發出了第一聲問好!
(爲何 print 輸出了兩遍呢?說實話我也不清楚,要不本身去查查資料,而後把答案反饋給我 :) )
有朋友說了,這個程序太簡單了,並且若是我想重複問好怎麼辦?難道每都把這段代碼拷貝、粘貼嗎?那好,讓咱們把這段代碼寫成一個函數 hi ,這樣,每次問好時只須要輸入 (hi) 就能夠了。
Common Lisp 程序中直接調用函數時通常要用括號把函數名括起來,好比 (hi)
咱們就直接在 REPL 界面來編輯剛纔輸入的內容,但是剛纔已經執行過這段代碼了,如今的 CL-USER> 提示符後面是空的,有朋友說:我就是不喜歡來回拷貝,但願能有一個快捷鍵來列出我輸入過的歷史命令,沒問題。
Emacs 中查詢歷史命令的快捷鍵是 M-p ,這裏的大寫 M 表示 Alt 鍵,M-p 就是同時按下 Alt 鍵 和 p 鍵 M-p 是向上翻 M-n 是向下翻
這樣就能夠把你在 REPL 中輸入過的歷史命令一一查看了,言歸正傳,歷史命令找回來了,但是光標跑到最後了,咱們須要把光標移動到最前面,我明白,你不想操做鼠標移動光標,但願有移動光標的快捷鍵,沒問題:
C-a 是把光標移動到行首的快捷鍵,這裏大寫的 C 表示 Ctrl 鍵,C-a 就是同時按下 Ctrl 鍵 和 a 鍵 C-e 是把光標移動到行尾的快捷鍵
恩,看來 Emacs 的鍵盤快捷鍵操做起來果真很流暢,那咱們繼續把代碼修改成函數:
CL-USER> (defun hi () (format t "你好,世界!")) HI CL-USER> (hi) 你好,世界! NIL
這裏涉及到自定義函數的知識點,我假設你們都學過 C 語言,那麼咱們能夠猜想一下 defun 的語法結構:
首先是括號,而後是 defun ,表示開始定義函數,再後面是 hi ,是咱們自定義的函數名稱,後面的空括號應該是參數吧,不過由於咱們這一段程序沒有使用參數,因此是空的,接着是函數體,也就是這個函數具體執行的操做,這個函數體要用括號括起來,最後再用一個括號和最前面的括號對應,把全部的內容括起來。 這裏咱們發現 REPL 在遇到回車換行時,它不會按行處理,而是按括號來處理,因此你能夠增長任意個回車換行,只要沒有輸入跟第一個左括號匹配的右括號,它都不會認爲你的輸入結束,只有當你全部的左括號都有一個對應的右括號來匹配時,REPL 纔會認爲你輸入的內容結束了。
這裏給一個 defun 函數定義的標準語法形式吧:
(defun name (parameter*) "可選的函數描述" body-form*) parameter* 表示 0 個或者多個 parameter,這裏的 * 是正則式語法符號,表示 0 個或多個 body-form* 表示 0個或多個 body-form
對應中文就是這樣:
(defun 函數名 (參數*) "可選的函數描述" 形式體*)
這裏解釋一下 body-form 這個概念,Common Lisp 定義了兩個語法黑箱,前者叫讀取器,將文本轉化爲 Lisp 對象,後者叫求值器,它用前者定義好的 Lisp 對象來實現語言的語義,咱們知道直接輸入到讀取器中的 Lisp 程序是由 S-表達式組成的,而求值器則定義了一種構建在 S-表達式(符號表達式)之上的 Lisp形式--form 的語法。
全部的字符序列都是合法的 S-表達式
這一點意味着你能夠把任意的字符序列交給 Lisp 的讀取器來處理,你能夠定義任意的語法格式來做爲你的程序的文本輸入---固然,這須要你作一些相關的設置,不過咱們仍是建議初學者先了解、熟悉你們都習慣的 Lisp 語法形式,等你真正學會了,就能夠創造本身的程序語言了,是否是聽起來很鼓舞鬥志?
可是並不是全部的 S-表達式 都是合法的 Lisp形式--form
舉個例子就清楚了,(hi) 和 ("hi") 都是合法的 S-表達式,可是 (hi) 是一個合法的 Lisp形式--form,而 ("hi") 就不是一個合法的 Lisp形式--form,由於字符串做爲列表的第一個元素對於 Lisp形式--form 而言是沒有意義的。
說了這麼多,其實主要是討論什麼纔是函數定義中的 body-form(形式體)的合法形式,想搞清這個問題,又不肯意多想的話,就到環境上去試驗吧,把你能想到的各類形式都試驗一下。:)
留個小做業:
若是函數定義中有多個形式體,應該如何去寫?建議你們本身上機試驗。
既然有通常狀況,那就有特殊狀況,另外一種對函數的調用方式是間接調用,就是把函數1做爲參數傳遞給另外一個函數,由另外一個函數間接調用函數1,具體到咱們這個例子就是用另外一個函數間接調用 hi,接下來就介紹這兩個函數:funcall 和 apply,它們須要使用這種形式:
CL-USER> (funcall #'hi) 你好,世界! NIL CL-USER> (apply #'hi nil) 你好,世界! NIL CL-USER>
感興趣的朋友能夠試着分析一下 funcall 和 apply 的區別,另外也能夠試着執行一下下面這兩種形式,看看會有什麼錯誤提示,從這些錯誤提示信息也能夠了解一些 Common Lisp 的內部處理機制。
犯錯嘗試: 錯誤1: (apply #'hi) 錯誤2: (funcall #'(hi)) 錯誤3: (apply #'(hi))
上面咱們提到一個組合符號 #' ,它由兩個符號組成,前面是井號 # ,緊跟着是單引號 ' ,這個組合符號 #' 等價於函數 function,前者是後者的「語法糖」,不過二者的使用形式有些區別:
CL-USER> (funcall (function hi)) 你好,世界! NIL CL-USER>
【小提示】:
在這些不起眼的處理細節上多想一想區別,多試試錯誤,多看看錯誤提示信息,有助於咱們更好的理解 Common Lisp 的內部處理機制。
關於 funcall 和 apply 更具體的應用場景就不在這裏詳述了,建議你們閱讀《實用 Common Lisp 編程》和 《ANSI Common Lisp》中的相關章節來作更深刻的瞭解。
哈,通過這麼一番學習,終於完成了咱們的第一個函數,因而有人就說:這麼好的函數能不能保存起來,省得下次想調用它還得從新輸入這麼多字符,沒問題:
能夠先拷貝程序文本,這裏說一下 Emacs 下的拷貝粘貼快捷鍵:
Mac 系統 拷貝是 Command 鍵 和 c 鍵同時按下 粘貼是 C-y : Ctrl 鍵 和 y 鍵 同時按下 是否是感受有些奇怪,不要緊,若是不適應的話能夠本身修改配置文件,或者修改 slime.el 文件來從新定義 MS-Windows 系統 拷貝是 M-w :Alt 鍵 和 w 鍵 同時按下 粘貼是 C-y :Ctrl 鍵 和 y 鍵 同時按下
而後建立新文件:
使用以下快捷鍵 C-x C-f 就是先同時按下 Ctrl 鍵 和 x 鍵,而後所有鬆開,再同時按下 Ctrl 鍵 和 f 鍵,再鬆開,Emacs 屏幕底部會顯示以下: Find file: ~/ 默認保存在當前用戶目錄下,Mac系統是 /Usrs/admin/
你輸入要保存具體要保存的目錄,個人文件保存在 ~/ECode/Markdown-doc/hi.lisp
能夠使用 TAB 鍵來自動補全,這樣就沒必要一個個手工輸入了
我輸入的文件路徑和名稱以下:
Find file: ~/ECode/Markdown-doc/hi.lisp
注意文件名後綴要保存爲 .lisp 表明這個文件是 Common Lisp 程序。
Emacs 也有一種用來定製編輯器的 Lisp 語言,叫作 Emacs Lisp,這種文件的後綴是 .el 或 .emacs
OK,輸入上述這些以後,回車,Emacs 就會建立一個名爲 hi.lisp 的 Lisp 源程序文件,放在 ~/usrs/admin/Ecode/Markdown-doc/ 目錄下。
注意,這時這個文件仍是一個空文件,把咱們以前拷貝好的程序內容,粘貼到這個新建的空文件裏。
而後就是執行文件保存的快捷命令了:
Mac 系統 C-x C-s 或者 Command-s MS-Windows 系統
很好,到如今爲止,你已經成功地寫出了第一個程序,而且對這個程序作了一些擴展,而後又成功地把它保存了起來,那麼接下來就要提到如何加載它了,咱們能夠使用 load 函數來進行加載。
這時又有朋友發現了,咱們剛纔使用的 REPL 界面不見了,被新開啓的 hi.lisp 的文本編輯界面所取代了,我想繼續回到剛纔那個 REPL 界面該怎麼辦?有多種快捷方法能夠調出剛纔的 REPL 界面,咱們先說一種最適合一邊在文本編輯界面寫代碼,一邊用 REPL 來調試的的調用方法,快捷鍵以下:
C-c C-z 能夠直接調出一個關聯到當前文本編輯界面的 REPL 窗口
爲何說特別適合調試呢?好比,你在文本編輯區寫了一段函數代碼,想馬上看看這段代碼的執行狀況,那你能夠把光標放在這個函數代碼段內的任意一個位置,而後輸入快捷鍵:
C-c C-y 把光標所在區域的函數名稱發送到對應的 REPL 進程中,很是方便調試代碼
這個函數名稱就自動跑到 REPL 去了,是這個樣子:
CL-USER> (hi )
看看連括號都沒拉下,並且函數名後面還自動加了個空格預防你一旦須要有參數輸入,而後直接回車就能夠在 REPL 中調試你剛寫好的函數了,是否是很方便?
好了,函數在 REPL 中調試過了,你也看到了執行效果,以爲還須要再加點什麼,因而又想切換回到文本編輯緩衝區了,那麼快捷鍵以下:
C-x o 先同時按下 Ctrl 鍵 和 x 鍵,鬆開,再按下 o 鍵
這樣就又切換回剛纔的文本編輯緩衝區了。
這裏我開始使用緩衝區(buffer)這個名詞,緩衝區是 Emacs 編輯器的一個概念,文本編輯窗口是一個緩衝區,REPL 是一個緩衝區,消息事件也是一個緩衝區,不一樣的緩衝區能夠來回切換,緩衝區的屏幕布局也能夠經過快捷鍵來設置:
C-x 1 當前緩衝區佔據整個 Emacs 窗口,其餘緩衝區所有放到後臺; C-x 0 關閉當前緩衝區 C-x 2 在當前緩衝區上方新打開一個緩衝區 C-x 3 在當前緩衝區右方新打開一個緩衝區
最後再介紹一個超級有用的快捷鍵:查看標準函數、標準宏源代碼,咱們知道 Common Lisp 的不少實現都是開源的,包括咱們推薦使用的 CCL,這就意味着咱們能夠查看其源代碼,既便於深刻理解,也便於學習模仿,好比對於自定義函數的宏 defun ,咱們想查看它的源代碼,想了解它的具體實現細節,能夠把光標放在 defun 上,而後按 M-.
M-. 同時按 Alt 鍵 和 點鍵 . ,查看當前光標所在位置的函數的源代碼
你們可能注意到我介紹了很多的 Emacs 快捷命令,這正是我想要大力推薦的,就個人使用經驗而言,這些快捷操做可以極大地提高 Emacs 開發環境下編程、調試的效率,因此但願初學者能熟悉這些快捷操做,其實多用幾回就熟悉了,慢慢就習慣了,不知不覺工做效率就提升了,能夠早點完成工做了,能夠早點下班回家了,因而有了更多的自由時間,能夠多看看書、多運動運動、多陪陪家人、多發展下我的的興趣愛好……而後你的整我的生就改變了 :)
美好的將來真值得期待啊,如今讓咱們言歸正傳,繼續討論如何加載 Lisp 源程序,有多種方式能夠加載,咱們先介紹在 REPL 界面的方式:
CL-USER> (load "~/ecode/markdown-doc/hi.lisp") #P"/Users/admin/ECode/Markdown-doc/hi.lisp" CL-USER> (hi) ä½ å¥½ï¼ä¸çï¼ NIL CL-USER> [說明 #P"/Users/admin/ECode/Markdown-doc/hi.lisp" 這種返回形式表示返回結果是一個路徑對象]
悲劇了,精心編寫的問候語成了一堆亂碼,是什麼緣由?該怎麼辦呢?緣由也簡單,全部的文件操做函數(好比 load open等)默認的字符編碼格式類型都是 :latin-1 ,這種編解碼類型對應英文字符,遇到中文內容天然就亂碼了。
沒錯,你沒看錯,我也沒寫錯,這個類型名稱就是以冒號打頭的一個符號,這種類型的符號在 Lisp 中被稱爲關鍵字 keyword ,對這種類型的符號求值會獲得冒號後面的符號名稱,從如今開始初學者要逐步適應 Lisp 對符號的使用習慣。
而咱們的中文使用的編碼偏偏不是這個,而是 :utf-8 ,那麼就須要手工指定了,以下:
CL-USER> (load "~/ecode/markdown-doc/hi.lisp" :external-format :utf-8) #P"/Users/admin/ECode/Markdown-doc/hi.lisp" CL-USER> (hi) 你好,世界! NIL CL-USER>
好了,問題解決了,但是有些朋友以爲很麻煩,每次加載文件都得輸入一長串額外的參數,這裏介紹一個稍微簡單點的辦法,能夠經過修改系統的全局變量 *default-external-format* 來把默認的文件格式改爲咱們須要的 :utf-8 ,先查看一下當前的值,以下:
CL-USER> *default-external-format* :UNIX CL-USER>
用 setq 函數修改,第一個參數是要修改的全局變量,第二個參數是但願修改爲的值,修改而後查看:
CL-USER> (setq *default-external-format* :utf-8) :UTF-8 CL-USER> *default-external-format* :UTF-8 CL-USER>
說明:全局變量 *default-external-format* 在 CCL 和 CLisp 中能夠用,可是在 SBCL 中不支持,所以若是你的編程環境是 SBCL 的話,那麼想要支持中文就須要每次手動指定編碼格式了---SBCL 是否有相似的全局變量?我不太清楚,知道的朋友能夠指點一下。
之因此介紹使用 setq 函數,是由於這個函數在 Common Lisp 和 Emacs Lisp 中均可以使用,均可以用於賦值,也就是說你能夠在 Emacs 的配置文件中使用 setq 這個函數來修改一些全局配置量,並且 setq 在 Common Lisp 中更是一個特殊操做符,聽說現代風格通常使用宏 setf 來實現賦值功能,setf 封裝了對 setq 的調用。更詳細的使用方法能夠查詢 HyperSpec 。
好了,再試一下,看看效果:
CL-USER> (load "~/ecode/markdown-doc/hi.lisp") #P"/Users/admin/ECode/Markdown-doc/hi.lisp" CL-USER> (hi) ä½ å¥½ï¼ä¸çï¼ NIL CL-USER> (setq *default-external-format* :utf-8) :UTF-8 CL-USER> (load "~/ecode/markdown-doc/hi.lisp") #P"/Users/admin/ECode/Markdown-doc/hi.lisp" CL-USER> (hi) 你好,世界! NIL CL-USER>
修改生效!擊掌慶祝一下!
賦值語句 setq 的各類例子:
CL-USER> (setq a (print "hello world!")) "hello world!" "hello world!" CL-USER> (setq a '(hello world!)) (HELLO WORLD!) CL-USER> (setq b (print "hello world!")) "hello world!" "hello world!" CL-USER> b "hello world!" CL-USER> (setq c '()) NIL CL-USER> c NIL CL-USER> (setq c ()) NIL CL-USER> c NIL CL-USER> (setq c ( )) NIL CL-USER> c NIL CL-USER> (setq c ( )) NIL CL-USER> c NIL CL-USER> (setq c ( nil )) ; Evaluation aborted on #<TYPE-ERROR #x302000CF368D>. CL-USER> (setq c (nil)) ; Evaluation aborted on #<TYPE-ERROR #x302000BF2BBD>. CL-USER> (setq c '(nil)) (NIL) CL-USER> c (NIL) CL-USER> (setq c (nil)) ; Evaluation aborted on #<TYPE-ERROR #x302000C812CD>. CL-USER> 【補充說明】事實上字符編解碼會涉及一系列知識點,我這裏只是大體提一下,只簡單介紹部分用法,不作詳細講解,感興趣的朋友能夠本身搜索相關資料,好好看看,把這些弄懂了基本上就清楚在各類不一樣場景下程序支持中文的機制了。
寫到這裏,就天然而然地涉及到了在程序中使用中文符號的話題,就個人理解,在程序中對中文符號的使用可分爲以下場景:
這個是最多見的一種使用方式,也是最容易實現的一種方式,咱們前面的問好程序 hi 就是把中文當字符串使用的
這個須要對開發環境作一些配置,由於你的 Lisp 的讀取器也好,求值器也好,都須要專門指定編解碼來識別雙字節的中文,並且使用 Slime 這種交互接口,還涉及一個客戶端和服務端通訊的編解碼,咱們接下來也主要講解在這種方式下使用中文符號須要進行的配置工做
這種場景下連 「if」 這樣的關鍵字均可以寫成中文 「若是」 了,這樣使用中文符號的結果是:源程序徹底由中文、數字和其餘符號組成,也就是說你能夠自由地使用中文進行編程,須要編譯器識別這些關鍵字,所以這種使用方式須要作更進一步的配置,幸運的是 Lisp 能夠經過一些簡單的設置完美地支持。
我正在投入的一個開源項目:【開源母語編程】 就是但願能在這方面作一些開拓性的工做,提供一個試驗性質的平臺,可讓對此感興趣的開發者以此項目爲基礎繼續深刻研究。
須要修改 Emacs 的配置文件,在 LispBox 環境中是經過 lispbox.el 文件進行配置的,在該文件中增長以下內容:
(set-language-environment "utf-8") (set-buffer-file-coding-system 'utf-8) (set-terminal-coding-system 'utf-8) (set-keyboard-coding-system 'utf-8) (set-selection-coding-system 'utf-8) (set-default-coding-systems 'utf-8) (set-clipboard-coding-system 'utf-8) (setq ansi-color-for-comint-mode t) (setq-default pathname-coding-system 'utf-8) (setq default-process-coding-system '(utf-8 . utf-8)) (setq locale-coding-system 'utf-8) (setq file-name-coding-system 'utf-8) (setq default-buffer-file-coding-system 'utf-8) (setq slime-net-coding-system 'utf-8-unix) (modify-coding-system-alist 'process "*" 'utf-8) (prefer-coding-system 'utf-8)
說實話,除了少數幾條配置的做用比較明確,我也不是很肯定上述這些配置具體哪條會起什麼做用,你們就根據名稱本身顧名思義一下吧,固然若是有人可以詳細整理一下每條的確切做用共享出來,那就最好不過了。 :)
修改完成後,重啓 LispBox,你的開發環境就支持使用中文字符作變量名和函數名了===沒錯,個人環境就是這樣設置的,如今咱們再把剛纔那個問好函數修改完善一下,增長下面這個函數:
(defun 你好 () (format t "你好,世界!--我使用了中文函數名稱!"))
而後使用 C-c C-k 在文本編輯緩衝區完成編譯,再使用 C-c C-y 把函數名稱發送到 REPL 進程上的輸入區域,回車,所有顯示以下:
CL-USER> ;Compiling "/Users/admin/ECode/Markdown-doc/hi.lisp" CL-USER> (你好 ) 你好,世界!--我使用了中文函數名稱! NIL CL-USER>
哈,恭喜你,終於能夠使用中文來自定義函數名稱了,話說我實在是受夠那些無比冗長的英文函數名稱了。
英文是一種一維的線性文字,適合聽覺處理而不適合視覺處理,想表達清楚一個含義,必須使用長長的一串字母組合,而中文的優點就是它是一種非線性的二維文字,相對來講更適合視覺處理(其實中文也適合聽覺處理,雖然有很多同音字,可是根據上下文能夠清晰肯定其確切含義)。
通常來講:2箇中文字符佔據4個字節,4個字節對應4個英文字母,4個英文字母所能表述的內容跟2箇中文字符所能表述的內容一對照就遜色不少了,在不影響清晰表達的前提下,用中文來作函數名能夠比英文縮短一半以上。
固然也有其餘類型的使用不一樣字節的中文編碼,還有可變字節的編碼,這些就不一一細述了,感興趣的朋友能夠自行查閱相關資料。
正是基於上述緣由,我建議寫代碼時更多使用中文字符,這樣能夠有效縮短你源代碼文件的長度,並且你的項目越大,這種效果越明顯,越有利於節能減排。 :)
【注意】:
切記!千萬不要在中文輸入狀態下輸入各類符號,好比漢字的圓括號--》 () ,若是在程序中誤用了漢字符號,也就是全角符號,會產生編譯錯誤!!由於半角符號和全角符號對應的內部編碼是不一樣的!!所以 Common Lisp 程序代碼中全部的中文字符之外的其餘標點符號必須使用英文方式輸入,也就是要使用半角符號,而不是全角符號。
這裏的中文字符之外的符號指的是半角括號、逗號、單引號、反引號、雙引號等符號,使用本教程的 .emacs 配置文件,會自動把中文符號和英文符號設置爲不一樣的顏色:
半角的英文符號一概爲藍色 全角的中文符號一概爲綠色
能夠來這裏下載 Emacs 配置文件:
https://github.com/FreeBlues/PwML/blob/master/.emacs
初學者最好使用一鍵式開發環境,能夠節省不少沒必要要的投入,推薦 LispBox
各類 Emacs 快捷鍵使用技巧能夠大幅提高你的工做效率
幾種特殊鍵的表明符號:
交互編程最重要的幾個快捷命令:
突然發現這章的小結寫不下去了,由於內容比較散,就留給感興趣的讀者本身去作小結,寫完了能夠發給我,咱們共同署名更新 :)
本章內容主要是對 Common Lisp 的程序結構和語法形式作一個直觀易懂的描述,但願能在初學者腦海裏迅速創建一些關於 Common Lisp 程序的初步觀念,幫助初學者對於 Common Lisp 這門程序語言儘快有一個粗略的印象。
在創建這種粗略印象後,再結合前一章講解過的在開發環境上調試程序的實踐知識,初學者就擁有了理論 + 實踐的雙重工具(固然是尚不完善的理論),具有了繼續深刻學習的基礎,可以以最小障礙跨越 Common Lisp 入門階段,有了這個基礎,初學者就能夠真正自由地去探索 Common Lisp 那複雜博大的體系(確實夠複雜,」標準函數「就有978個,再加上各類不一樣實現的一些函數就更多了)。
當初學者學到這種程度,再回過頭來看這些入門階段的基本概念,可能以爲它們的表述不夠確切、不夠嚴謹----看來時機已經成熟,新世界即將在你面前打開:
歡迎進入新世界,尼歐!
既然你已經可以從更深層次來對 Common Lisp 進行理解和表述,那就把這些入門階段的基本概念都刷新一遍吧! (突然發現,「尼歐」 的中文拼音 」niou「 在搜狗輸入法中會對應到漢字「牛」,哈,估計以前沒有人發現過這個神祕巧合吧!--「牛」字正規的漢語拼音應該是 「niu」)
就像大多數人最初學習數學時,數學老師會告訴你:對負數開平方是非法的,可是一旦學到複數的層次,負數也能夠開平方了,並且還有特別的含義--旋轉量,處於不一樣階段對於知識的理解天然也不盡相同,這是一種螺旋式上升(迭代開發的另外一種表述),在哲學上這就是否認之否認。
【補充一句:】
初學者首次閱讀本章時,本章內容能看懂多少算多少,沒必要過於深究,其實有個大概的印象就能夠了,而後就能夠直接開始第四章的學習+實踐,在實踐的過程當中可能有些概念就天然而然地理解了。畢
基本概念:Common Lisp 程序在語言處理器的不一樣處理階段有不一樣的形式體現,在讀取器中識別的是 S-表達式,在求值器中識別的是 Lisp形式--form ,S-表達式的基本元素是列表(list)和原子(atom)。關於 S-表達式 和 Lisp形式 form 的區別以前咱們稍微討論過一些。
對於初學者來講,Common Lisp 程序就是用無數括號括起來的各類符號表達式,括號裏的 S-表達式 能夠有這麼幾種組合:純粹的原子,純粹的列表,列表和原子,以下:
(原子1 原子2 原子3) (列表1 列表2 列表3) (原子1 列表1 原子2 原子3 列表2)
可是前面也說了,並不是全部的 S-表達式 都是合法的 Lisp形式--form ,這裏咱們彷佛能夠給 Lisp形式--form 下一個簡單的定義:
可以在 REPL 求值器里正常求值的 S-表達式 纔是合法的 Lisp形式--form
有了這個定義,咱們就能夠直接在 REPL 中試驗了,把你想要驗證的 S-表達式 輸入到 REPL 中,而後回車
理論上說,lisp程序形式能夠由任何符號形式組成,不過對於初學者而言,暫時還不必深究這些,就老老實實地使用括號語法吧。
關於 Lisp 括號的笑話:話說一名黑客冒死竊取到美國核彈控制程序的最後一頁,打開一看,滿滿一頁右括號。。。
:)
《GNU Emacs Lisp 編程入門》中是這麼說的:
「列表由 0 個或者更多的原子或者內部列表組成,原子或者列表之間由空格分隔開,並由括號括起來。列表能夠是空的」。
《實用 Common Lisp 編程》中是這麼說的:
「S-表達式的基本元素是列表和原子。列表由括號所包圍,並可包含任何數量的由空格所分隔的元素。列表元素自己也能夠是 S-表達式--也就是原子嵌套的列表」 「任何原子(非列表或空列表)都是一個合法的 Lisp形式」
這就是列表的句法規則(syntax)
《GNU Emacs Lisp 編程入門》中是這麼說的:
原子是多字符的符號(如 forward-paragraph)、單字符符號(如 + 號)、雙引號之間的字符串、或者數字。
這裏補充一下,Emacs Lisp 和 Common Lisp 的原子概念有所不一樣,Common Lisp 中有一個名爲 atom 的函數,能夠用來判斷是否原子,使用方式以下:
CL-USER> (atom 'sss) T CL-USER> (atom (cons 1 2)) NIL CL-USER> (atom nil) T CL-USER> (atom '()) T CL-USER> (atom 3) T CL-USER> (atom +) NIL CL-USER> (atom "qwert qwer") T CL-USER> (atom -) NIL CL-USER>
實際試驗一下就會發現,在 Common Lisp 中,單字符符號,如 + 號,是不被判斷爲原子類型的。
(atom object) 等價於 (typep object 'atom) 等價於
(not (consp object)) 等價於
(not (typep object 'cons)) 等價於
(typep object '(not cons))
說明:(typep object 'atom) 這條語句的含義是: object 是否爲類型 atom,typep 就是一個關於類型的謂詞判斷函數,由於 atom 既是一個 函數,又是一種 類型 ,在這條語句中 atom 做爲 類型 來使用。
前面一再提到「求值」,那麼什麼是求值?在這一點上 Emacs Lisp 和 Common Lisp 的差別較大,前者相對簡單,使用解釋方式,後者相對複雜,既能夠使用解釋方式,也能夠採用編譯方式,不少實現都採用編譯方式。
《Emacs Lisp 編程入門》中的描述以下:
當 Lisp 解釋器處理一個表達式時,這個動做被稱做「求值」。咱們稱,解釋器計算表達式的值。 對數字求值就是它自己 對雙引號之間的字符串求值也是其自己 當對一個符號求值時,將返回它的值 當對一個列表求值時,lisp解釋器查看列表中的第一個符號以及綁定在其上的函數定義。而後這個函數定義中的指令被執行。(這裏指的是列表中第一個符號是一個函數的場景)
《實用 Common Lisp 編程》中給出一種便於理解討論的描述以下 :
爲了便於討論,你能夠將求值器想象成一個函數,它接受一個句法良好定義的 Lisp形式 做爲參數並返回一個值,咱們稱之爲這個形式的值。固然,當求值器是一個編譯器時,狀況會更加簡化一些----在那種狀況下,求值器被給定一個表達式,而後生成在其運行時能夠計算出相應值的代碼。 原子可分爲符號和全部其餘類型,符號在做爲 Lisp形式 被求值時會被視爲一個變量名,而且會被求值爲該變量的當前值(符號宏 symbol macro 有不一樣的求值方式,不過新手能夠暫不不去理會)。 全部非符號類型的原子,好比數字和字符串,都是自求值對象,這就意味着當這樣的表達式被傳遞給咱們假想的求值函數時,它會簡單地直接返回自身。 當咱們開始考慮列表的求值方式時,事情變得更加有趣了。全部合法的列表形式均以一個符號開始,可是有三種類型的列表形式,它們會以三種至關不一樣的方式進行求值。爲了肯定一個給定的列表是哪一種形式,求值器必須檢測列表開始處的那個符號是(列表的第一個符號)什麼類型:是函數、宏、仍是特殊操做符的名字。若是該符號還沒有定義,好比說當你正在編譯一段含有對還沒有定義函數的引用代碼時,它會被假設成一個函數的名字。這三種類型的形式稱爲函數調用形式、宏形式和特殊形式。
簡單地說,你在 REPL 中輸入一個 Lisp 形式--form,而後敲回車,就啓動了一個求值過程,若是你輸入的是一個符號原子,那麼 Lisp 會把其當作一個變量處理,返回該變量的當前值,若是你輸入的是一個非符號原子(自求職對象),那麼 Lisp 會直接返回該對象自身。
這裏再對「對自求值對象求值時,它會簡單地返回自身」補充一點說明:
《實用 Common Lisp 編程》中提到:
對於一個給定類型的數字來講,它能夠有多種不一樣的字面表示方式,全部這些都將被 Lisp 讀取器轉化成相同的對象表示。例如,你能夠將整數 10 寫成 十、20/二、#xA 或是其餘形式的任何數字,但讀取器將把全部這些轉化成同一個對象。當數字被打印回來時,好比在 REPL中,它們將以一種可能與輸入該數字時不一樣的規範化文本語法被打印出來。以下: CL-USER> 10 10 CL-USER> 20/2 10 CL-USER> #xa 10 CL-USER>
《GNU Emacs Lisp 編程入門》中是這麼說的:
當對一個函數求值時老是返回一個值(除非獲得一個錯誤消息)。另外,它也能夠完成一些被稱做附帶效果的操做。在許多狀況下,一個函數的主要目的是產生一個附帶效果。
《實用 Common Lisp 編程》中說得更詳細一些 :
函數調用形式的求值規則很簡單 ,對以 Lisp形式 存在的列表其他元素進行求值並將結果傳遞到命名函數中(也就是列表的第一個元素)。 當列表的第一個元素是一個由特殊操做符所命名的符號時(簡單說就是一個特殊操做符),表達式的其他部分將按照該操做符的規則進行求值。 先說一下宏,宏是一個以 S-表達式 爲其參數的函數,並返回一個 Lisp形式,而後對其求值並利用該值取代宏形式。 宏形式求值過程包括兩個階段:首先,宏形式的元素不經求值即被傳遞到宏函數裏;其次,由宏函數所返回的形式(稱其爲展開式)按照正常的求值規則進行求值。
《GNU Emacs Lisp 編程入門》中是這麼說的:
單引號告訴lisp解釋器返回後續表達式的書寫形式,而不是像沒有單引號那樣對其求值。
《實用 Common Lisp 編程》中是這麼說的:
單引號是 quote 語句的語法糖。
也就是說,形如 '(1 2 3) 的語句實際上就是 (quote (1 2 3 )),實際執行效果同樣,以下:
CL-USER> (quote (1 2 3 )) (1 2 3) CL-USER> '(1 2 3) (1 2 3) CL-USER>
如今咱們使用的 SBCL 和 CCL 中,都把單引號設置爲 quote 的語法糖,也就是說,在這兩種實現中,咱們能夠很方便地用單引號來代替 quote ,經過上面的例子能夠很清楚地看到,使用語法糖能夠簡化代碼,因此咱們推薦初學者在代碼中多使用語法糖。
Common Lisp 實際上提供了修改這種對應關係的宏,也就是說你能夠爲 quote 設置其餘不一樣的符號來作語法糖,不過對於常見的程序開發來講,不必修改,並且要儘可能避免修改這種對應關係。
【說明】:這10條小結是 《GNU Emacs Lisp 編程入門》第一章總結出來的,該書主要講解 Emacs Lisp 的基本概念和應用,客觀地說 Emacs Lisp 跟 Common Lisp 雖然都是從 Lisp 演化而來,但仍是存在着很大差別的,不過我在學習過程當中發現對於 Common Lisp 初學者而言,能夠拿 Emacs Lisp 一塊兒來對照學習,尤爲是對一些基本概念的理解,很是有幫助,並且若是 Common Lisp 初學者選擇了 Emacs 做爲開發環境,那你確定須要熟悉 Emacs Lisp 的使用,不然就沒法充分利用 Emacs 的高效做業。所以專門拿了一個章節來介紹 Emacs Lisp 的這些基本概念,但願初學者們能從中獲得助益。
本章以一個 Common Lisp 的實際例程爲主要內容,開發過程也儘可能採用最適合 Common Lisp 的逐步完善、反覆迭代的開發方式。 這個例子基本上所有代碼都照搬了《實用 Common Lisp 編程》中的第三章「實踐:簡單的數據庫」的內容,不過我按照本身的講解方式稍做修改,看看這種講述風格是否能被你們接受,同時把 CD 數據庫改成書籍數據庫,由於我以爲對於喜歡閱讀的中國讀者來講,書籍數據庫可能更實用一些。
在此要感謝做者 Peter Seibel 和譯者 田春 的努力,爲咱們提供了這麼好的教程,若是做者或譯者對我大量直接引用他們二位的例程有異議,請告知,我會刪除重寫一個例程--不過仍是但願能獲得二位的贊成 :)。
摘錄一句做者 Peter Seibel 的原文:
「本章的重點和意圖也不在於講解如何用 Lisp 編寫數據庫,而在於讓你對 Lisp 編程有個大體的印象,並能看到即使相對簡單的 Lisp 程序也能夠有着豐富的功能」
不少朋友都喜歡買實體書閱讀,長此以往家裏的藏書就愈來愈多,佔滿了書架,不得不把一些舊書打包到箱子裏放到牀下,可是有時突然想查找某本書的內容時才發現一時找不到這本書了,因而只好翻箱倒櫃一個箱子一個箱子查看,最後終於找到了,可是費了半天勁,並且搞得滿身灰塵,而後坐在電腦前小憩時突然發現原來 F:\ 盤的電子書目錄裏有這本書的電子版,嘿,是否是以爲特別坑爹。
很好,咱們這個小型項目就是教你如何去創建一個藏書數據庫,把你的全部藏書信息都輸入到電腦裏,包括書名、做者、內容簡介、價格、購買時間、實體書保存位置、是否有電子版、電子版保存位置等等信息,同時提供查詢的功能,能夠根據關鍵字進行檢索,擁有這樣一個藏書數據庫是否是會提升你對藏書的使用效率呢?
那就讓咱們一塊兒開始吧!
咱們已經瞭解了本身的軟件需求,那麼接下來就是選擇相應的數據結構了,須要選擇一種方式來表示每一條數據庫記錄,而每條數據庫記錄要包含上述提到的內容:
(書名、做者、內容簡介、購買時間、價格、實體書保存位置、是否有電子版、電子版保存位置)
上面的內容怎麼看怎麼像個列表啊,把分隔每一個項目的頓號去掉換成空格,它不就是一個列表嗎?
(書名 做者 內容簡介 購買時間 價格 實體書保存位置 是否有電子版 電子版保存位置)
不過爲了簡化程序輸入,咱們把上述列表項目稍做縮減,程序中使用以下列表:
(書名 做者 價格 是否有電子版)
既然天意讓它看起來這麼像一個列表,那咱們就使用列表做爲基本的數據結構。
列表知識:咱們能夠使用 list 函數來生成一個列表,正常執行後,它會返回一個尤爲參數組成的列表,以下:
CL-USER> (list 1 2 3 4) (1 2 3 4) CL-USER>
不過鑑於咱們但願在使用每條記錄的每一個字段時都能有對該字段的一個明確的描述,而不是必須使用數字索引來訪問,因此咱們選擇一種被稱爲屬性表(property list,plist)的列表,這種列表中的第1個元素用來描述第2個元素,第3個元素用來描述第4個元素,以此類推,第奇數個元素都是用來描述相鄰的第偶數個元素的,換句話說就是:從第一個元素開始的全部相間元素都是一個用來描述接下來那個元素的符號(原文引用 :)),在 plist 裏奇數個元素的寫法使用一種特殊的符號--關鍵字符號(keyword)。
關鍵字符號是任何以冒號開始的名字,例如---》 :書名
下面是一個使用了關鍵字符號做爲屬性名的示例 plist :
CL-USER> (list :書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 t) (:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T) CL-USER>
這裏要提到一個屬性表的函數 getf ,它能夠根據一個 plist 中的某個字段名(屬性名)來查詢對應的屬性值,以下所示,咱們想要查詢剛纔創建的 plist 中的 :書名 屬性名所對應的屬性值:
CL-USER> (getf (list :書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 t) :書名) "人間詞話" CL-USER>
若是想查 :做者 是什麼,輸入以下:
CL-USER> (getf (list :書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 t) :做者) "王國維" CL-USER>
有了上述這些基本知識,咱們就能夠寫出一個簡單的名爲 創建書籍信息 的函數了,它以參數的形式接受 4 個屬性字段,而後返回一個表明該書的 plist:
(defun 創建書籍信息 (書名 做者 價格 是否有電子版) (list :書名 書名 :做者 做者 :價格 價格 :是否有電子版 是否有電子版))
咱們定義了這個函新數,函數名是 創建書籍信息 ,跟在函數名後面的是形參列表,這個函數有 4 個形參,分別是: 書名、做者、價格、是否有電子版,這個示例中的函數體只有一個 Lisp形式--form ,這個惟一的 Lisp形式 就是對函數 list 的調用。當函數 創建書籍信息 被調用時,傳遞給該調用的 4 個實參將被綁定到形參列表中的變量上。好比爲了創建剛纔那本《人間詞話》的書籍信息,咱們能夠這樣調用這個函數:
CL-USER> (創建書籍信息 "人間詞話" "王國維" 100 t) (:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T) CL-USER>
列表實際上有 8 個元素,第奇數個元素是 字段名 ,第偶數個元素是 字段值,奇數位置的 字段名 使用關鍵字符號表示(以冒號打頭),偶數位置的 字段值 則根據實際類型來選擇 字符串、數字、布爾值 來表示。
爲了例程書寫方便,咱們只使用了 4 個字段來記錄書籍信息,可是對於一本書來講,4 個字段的信息固然有些不足,後續你們能夠自行增長其餘字段。
只有一條記錄的數據庫對應一個 plist 列表,只能記錄一本書的信息,顯然沒法知足咱們的實際需求,所以咱們準備使用全局變量來記錄多個列表的信息,每一個列表就是一條記錄,保存一本書的信息。
在 Common Lisp 中,全局變量的命名約定是名字先後各加一個星號 * ,這樣的形式:
*書籍數據庫*
全局變量能夠使用宏 defvar 來定義,初值爲 nil,以下:
(defvar *書籍數據庫* nil)
咱們能夠使用宏 push 來爲全局變量 *書籍數據庫* 增長新的記錄,可是這裏但願能稍微作得抽象一些,因而就要定義一個函數: 增長記錄,具體定義以下:
(defun 增長記錄 (書籍信息) (push 書籍信息 *書籍數據庫*))
很好,如今就能夠把兩個函數結合在一塊兒使用了,先用 創建書籍信息 創建一條書籍信息的記錄,該函數返回一個 plist,再把這個 plist 做爲函數 增長記錄 的輸入參數,由函數 增長記錄 把該條數據添加到用全局變量 *書籍數據庫* 中。
CL-USER> (增長記錄 (創建書籍信息 "人間詞話" "王國維" 100 t)) ((:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER> (增長記錄 (創建書籍信息 "說文解字" "許慎" 100 t)) ((:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T) (:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER> (增長記錄 (創建書籍信息 "難忘的書與插圖" "汪家明" 38 t)) ((:書名 "難忘的書與插圖" :做者 "汪家明" :價格 38 :是否有電子版 T) (:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T) (:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER>
爲何每次執行完後,會把整個數據庫的內容都返回呢?由於每次執行這段增長記錄的代碼實際上執行的是 push 這個宏,而 push 所修改的全局變量 *書籍數據庫* ,實際上是這樣一個大列表 ((plist1) (plist2) (pist3)),push 會把它正在修改的變量的新值返回,對 push 來講,它修改的變量就是這個大列表---它每次在裏面增長一個小列表,所以每次執行後都會把它修改後的大列表整個返回。
此時咱們能夠在 REPL 中輸入全局變量 *書籍數據庫* 來查看它的當前值以下:
CL-USER> *書籍數據庫* ((:書名 "難忘的書與插圖" :做者 "汪家明" :價格 38 :是否有電子版 T) (:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T) (:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER>
可是很顯然,這種查看輸出的方式有些凌亂,咱們能夠新一個名爲 轉儲顯示 的函數來把數據庫內容稍微整理一下顯示格式,而後再輸出,但願效果以下:
書名: 難忘的書與插圖 做者: 汪家明 價格: 38 是否有電子版: T 書名: 說文解字 做者: 許慎 價格: 100 是否有電子版: T 書名: 人間詞話 做者: 王國維 價格: 100 是否有電子版: T
該函數以下所示:
(defun 轉儲顯示 () (dolist (單條書籍記錄 *書籍數據庫*) (format t "~{~a: ~20t~a~%~}~%" 單條書籍記錄)))
該函數的工做原理是使用 dolist 宏在 *書籍數據庫* 的全部元素上循環,依次綁定每一個元素到變量 書籍字段信息 上,而後再用 format 函數打印出每一個 書籍字段信息 的值。
這裏稍微介紹一下 format 的語法,它就像 C 語言中的函數 printf 同樣,使用格式控制字符來實現格式化輸出。
format 函數的第一個實參是它的輸出目的地,這裏是 t ,是一個簡稱,表示標準輸出流 *standard-output* ,它的第二個實參是一個格式字符串,格式字符串也是一個用雙引號引發來的字符串,爲了區別於通常的字符串,它使用 ~ 符號來標識格式指令(相似於 printf 函數的格式指令 %)。
下面針對函數 轉儲顯示 中使用的這條 format 進行解析:
(format t "~{~a: ~20t~a~%~}~%" 單條書籍記錄)))
首先明確一點,全部的格式指令都以 ~ 符號開始,各指令具體含義以下所示:
~{ format 的循環語法,表示下一個對應的實參是一個列表的開始,而後 format 會在該列表上進行循環操做,處理位於 ~{ 和 ~} 之間的指令,每輪循環處理多少個實參取決於 ~{ 和 ~} 之間有多少個對應實參的指令,執行多少輪循環取決於 「單挑書籍記錄」 中的元素的個數(確切說:循環輪數 = 元素個數 除以 每輪循環處理實參個數),因此能夠經過使用 ~{ 和 ~} 來實現循環, ~} 同上,和 ~{ 配合使用 ~a 美化指令,該指令對應一個實參,會把這個實參的顯示形式輸出爲更適合閱讀的形式,具體說就是形如 :書名 的關鍵字在輸出時會被去掉冒號,形如 "人間詞話" 的字符串在輸出時會被去掉雙引號 ~t 表示製表指令,不對應實參,只移動光標,~20t 告訴 format 把光標向後移動 20 列 ~% 表示換行,不對應實參 另外要注意 格式指令字符串中全部的非格式指令均以原樣輸出,好比 ~a 後面的冒號 : 和空格就直接原樣輸出
再對照咱們上述的代碼,就比較清楚了,首先是一個大循環:
(dolist (單條書籍記錄 *書籍數據庫*) ( 。。。)))
每輪大循環都從 *書籍數據庫* 裏取出一條記錄,把該條記錄的內容賦值給(綁定到)變量 單條書籍記錄 ,其內容以下:
(:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)
而後再進入函數 format 內的小循環,咱們看到在表示小循環的 ~{ 和 ~} 之間,有多個格式指令,可是隻有兩個 ~a 指令須要對應兩個實參,其餘格式指令分別用於移動光標和換行,因此每輪小循環處理兩個字段,像這個例子就是:
1)當 format 看到 ~{ 就進入第一輪小循環;
2)先處理 :書名 和 "人間詞話" 這兩個字段,第一個 ~a 指令把 :書名 字段的冒號 : 去掉,輸出 書名 ;
直觀演示一下: CL-USER> (format t "~{~a: ~20t~a~%~}" (list :書名 "人間詞話")) 書名: 人間詞話 NIL CL-USER>
3)緊跟着 ~a 指令的 冒號 和 空格 原樣輸出;
換個符號,把第一個 ~a 後面的冒號空格換成 ====》試試: CL-USER> (format t "~{~a====》 ~20t~a~%~}" (list :書名 "人間詞話")) 書名====》 人間詞話 NIL CL-USER>
4)指令 ~20t 則把光標右移20列;
把 ~20t 換成 ~50t 試試: CL-USER> (format t "~{~a: ~50t~a~%~}" (list :書名 "人間詞話")) 書名: 人間詞話 NIL CL-USER>
5)第二個 ~a 指令把 "人間詞話" 字段的雙引號 "" 去掉,輸出 人間詞話 ;
6)指令 ~% 則執行換行;
7)而後 format 看到 ~} ,知道本輪循環結束;
8)此時由於 單條書籍記錄 中還剩下後面 6 個字段,因而啓動第二輪小循環;
9)此次處理 :做者 和 "王國維" 這兩個字段;
10)接下來的處理跟上述的處理相似 。。。
11)第三輪小循環處理 :價格 和 100 這兩個字段;
11)一直到第四輪小循環,處理完 :是否有電子版 和 T 這兩個字段;
12)這時 單條書籍記錄 的全部元素都已經完成處理,就結束小循環,執行最後的 ~% ,換行。
而後就是下一輪大循環,再從 *書籍數據庫* 裏取出第二條記錄,而後把第二條記錄的內容賦值給變量 *書籍數據庫* ,而後就再次進入小循環,。。。就這樣反覆循環,直到把 *書籍數據庫* 中的全部記錄都循環一遍,這時就完成了大循環。
看了上述的分析,你就會發現,其實那個大循環並非必定要有的,徹底能夠把全部的循環操做都放在 format 中處理,讓 format 直接在 *書籍數據庫* 這個大列表上循環處理其中每一個小列表中的字段信息,代碼以下:
(defun 轉儲顯示 () (format t "~{~{~a: ~20t~a~%~}~%~}" *書籍數據庫*)))
修改很簡單,首先是在 format 原來的格式字符串最外圍增長一對 ~{ 和 ~} ,其次就是循環的對象由原來的 單條書籍記錄 改成 *書籍數據庫* ,兩種形式均可以,不過就我我的而言,比較推薦第一種,由於看起來更清晰,更具有可讀性。
程序寫到這裏,已經可以接受信息輸入、把信息儲存到數據庫、顯示數據庫的信息到屏幕,能夠說初具規模了,但是有些朋友可能會以爲那個輸入方式的界面太不友好,什麼提示也沒有,並且若是一旦須要大量輸入時,這種操做不太方便,也可能出錯,因此提出但願能在這裏迭代一下---把舊的輸入函數改形成一個更好用的、有提示的輸入界面,很好,下面咱們先寫一個帶提示信息的輸入接口:
(defun 提示輸入 (提示信息) (format *query-io* "~a: " 提示信息) (force-output *query-io*) (read-line *query-io*))
首先使用 format 產生一個提示,而後用 force-output 保證在不一樣 Common Lisp 實現中都表現出相同的效果---確保 Lisp 在打印提示信息前不會等待用戶輸入換行。
而後使用函數 read-line 來讀取單行文本。變量 *query-io* 是一個含有關聯到當前終端的輸入流的全局變量。
把這個 提示輸入 函數和咱們前面的 創建書籍信息 函數組合起來,構造出一個新函數,每次輸入前都會提示應該輸入哪一個字段,以下:
(defun 提示書籍信息 () (創建書籍信息 (提示輸入 "書名") (提示輸入 "做者") (提示輸入 "價格") (提示輸入 "是否有電子版[y/n]")))
這樣在輸入每一個字段都增長一個對應的字段內容提示信息,用起來就不容易輸錯了。
在這裏《實用 Common Lisp 編程》的做者專門說起用戶輸入驗證的問題,並據此對 價格 字段和 是否有電子版 字段的輸入函數作了針對性的修改。
我以爲做者在此處的講解表現出至關不凡的專業素養!每個初學者都應該把這一頁內容反覆理解,儘可能培養本身的這種對於用戶輸入驗證精益求精的態度,這是評價一個軟件是到底是一個玩具軟件仍是商用級別的工業軟件的關鍵標準!
養成這種良好的習慣,你編寫的哪怕是一個最小規模的的軟件從一開始就不會由於用戶的各類錯誤輸入而意外崩潰,健壯性是很是、很是重要的!
而這種細節習慣的養成也將會節省你大量的返工時間---雖然在開始時要多花一些時間去考慮各類輸入場景,不過我建議若是作商業開發能夠把用戶輸入驗證這部分功能代碼作成統一的模塊,由專人負責維護,其餘人直接重用就好了,這樣能夠兼顧健壯性和效率,若是是我的開發者也能夠專門投入必定時間把這一部分模塊化,之後每次直接使用就能夠了。
健壯的格式以下:
(defun 提示書籍信息 () (創建書籍信息 (提示輸入 "書名") (提示輸入 "做者") (or (parse-integer (提示輸入 "價格") :junk-allowed t) 0) (y-or-n-p "是否有電子版[y/n]: ")))
小做業:
:價格 字段在上面的輸入數據驗證中是當作整數處理的,實際實際的價格很是可能不是整數,如今雖然大多數圖書標價都是整數,可是一打折不就有小數出來了?因此這裏真正須要的是能知足整數和小數的輸入驗證,就當作做業,本身去思考怎麼驗證吧。
上面修改後的輸入函數能夠很好地提示和驗證,可是有個問題,就是每輸入一本書的信息就須要執行一次,批量輸入時豈不是很繁瑣?那咱們就把它做成循環的輸入接口好了。
批量輸入代碼以下:
(defun 批量輸入 () (loop (增長記錄 (提示書籍信息)) (if (not (y-or-n-p "還要繼續輸入下一本書籍的信息嗎?[y/n]: ")) (return))))
執行效果以下:
CL-USER> (批量輸入 ) 書名: 血色黃昏 做者: 老鬼 價格: 25 是否有電子版[y/n]: n 還要繼續輸入下一本書籍的信息嗎?[y/n]: (y or n) n NIL CL-USER>
咱們上面創建的數據庫依賴於全局變量 *書籍數據庫* ,全部的數據庫信息都儲存在內存裏,一旦重啓 Common Lisp 環境,這些數據就所有丟失了,所以爲了能在重啓後保持數據庫不丟失,咱們準備把創建在內存裏的數據庫以一個文本文件的形式保存到硬盤上,這樣就能夠在重啓後加載這個文本文件形式的數據庫到內存,避免了每次都要從新輸入的煩惱,代碼以下:
(defun 保存數據庫 (帶路徑的保存文件名) (with-open-file (文件綁定變量 帶路徑的保存文件名 :direction :output :if-exists :supersede) (with-standard-io-syntax (print *書籍數據庫* 文件綁定變量))))
該函數的實參 帶路徑的保存文件名 是一個含有用戶打算用來保存數據庫的文件名字符串,在 MS-Windows 和 Mac OSX 操做系統上,應該攜帶文件路徑,好比在 Mac OSX 系統下應該這樣調用:
CL-USER> (保存數據庫 "~/ecode/markdown-doc/book-db.txt") ((:書名 "血色黃昏" :做者 "老鬼" :價格 "25" :是否有電子版 "n") (:書名 "難忘的書與插圖" :做者 "汪家明" :價格 38 :是否有電子版 T) (:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T) (:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER>
在 MS-Windows 系統下應該這樣調用:
CL-USER> (保存數據庫 "F://ecode//markdown-doc//book-db.txt")
至於數據庫文件名,我這裏使用了 book-db.txt ,之因此選擇保存爲 txt 格式的文件,是爲了方便查看其內容,隨便找一個文本編輯器就能夠打開 txt 文本文件,其實你能夠使用任何一種後綴名,甚至能夠自定義一種後綴名,專門做爲這個程序的數據庫文件格式。
這裏用到了 print 函數,它會將 Lisp對象 打印成一種能夠被 Lisp讀取器 讀回來的形式。
這段代碼的具體操做就是:
1)首先,宏 with-open-file 根據咱們輸入的參數 帶路徑的保存文件名 打開一個文件,而後將文件流綁定到 文件綁定變量 上;
2)接着會執行一組表達式,就是這個:
(with-standard-io-syntax (print *書籍數據庫* 文件綁定變量)
3)這組表達式執行的操做以下:宏 with-standard-io-syntax 確保對函數 print 的一致性使用---有些特定的變量的值可能會影響函數 print 的行爲,如今由宏 with-standard-io-syntax 把這些特定變量所有設置爲標準值,代碼 (print *書籍數據庫* 文件綁定變量) 則把 *書籍數據庫* 的內容打印到 文件綁定變量 ,由於 文件綁定變量 綁定到了咱們新打開的文件上,因此實際上就把 *書籍數據庫* 的內容寫入到文件中了;
4)執行完這組表達式,再由宏 with-open-file 關閉文件。
看到這裏就會發現這個宏 with-open-file 有個好處,就是不須要咱們手動關閉文件,它會自動關閉,很是環保啊,之後必定要多用這個宏! :)
既然寫好了保存函數,那就再寫一個加載函數,代碼以下:
(defun 加載數據庫 (帶路徑的加載文件名) (with-open-file (文件綁定變量 帶路徑的加載文件名) (with-standard-io-syntax (setf *書籍數據庫* (read 文件綁定變量)))))
加載代碼的操做跟保存代碼相反,不過使用了相似的宏和函數,就再也不詳述了。
值得注意的是這兩個函數 print 和 read
print 能夠打印 Lisp對象,以一種 Lisp讀取器 能夠讀取的形式。
read 能夠從流中讀入數據,它使用與 REPL 相同的 讀取器,能夠讀取咱們在 REPL 提示符下輸入的任何表達式。
有了方便、友好的批量輸入函數,意味着咱們的藏書數據庫中的書籍信息記錄可能會愈來愈多,這樣若是每次使用函數 轉儲查看 想查看有哪些書時就不得不面對滿屏的信息了,是否是感受不太方便?記得好像有本書,講的主題就是《數量一多,一切就都不同了》,咱們也遇到了第一個瓶頸---數量帶來的麻煩。
那就想辦法解決這個小瓶頸吧,再次進入咱們的迭代流程,咱們須要實現的就是一個可以按照給出條件進行篩選查找的函數,好比你此次查書籍數據庫只是想找一找 王國維 寫的書,換句話說,就是根據 :做者 "王國維" 這組數據進行查找,寫成函數就是:
(查找 :做者 "王國維")
Common Lisp 提供了這樣一個函數 remove-if-not ,它有兩個參數,第一個參數是一個 謂詞,第二個參數是一個 列表,它會返回一個僅包含 原始列表 中匹配該 謂詞 的全部元素的新列表,舉個簡單的數字例子,有一個由一些天然數組成的列表,咱們想把其中全部的偶數取出來,以下所示:
CL-USER> (remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9 10)) (2 4 6 8 10) CL-USER>
這裏的 謂詞 是函數 evenp ,當它的參數是偶數時返回真,符號 #' 在前面提到過,是一個表示後續符號是函數的符號,等價於函數 function ,表示要把函數 evenp 做爲參數,也能夠寫成這樣:
CL-USER> (remove-if-not (function evenp) '(1 2 3 4 5 6 7 8 9 10)) (2 4 6 8 10) CL-USER>
若是沒有函數 evenp,或者你不知道這個函數,也能夠本身寫一個匿名函數,以下:
CL-USER> (remove-if-not #'(lambda (x) (= 0 (mod x 2))) '(1 2 3 4 5 6 7 8 9 10)) (2 4 6 8 10) CL-USER>
在這裏咱們首次提到了 匿名函數,它是這樣一種形式:lambda 跟 defun 的語法很是接近,lambda 後面緊跟着形參列表,而後是函數體。
也就是說,咱們如今須要寫的函數 查找 ,它會去逐條對比數據庫中的記錄,遇到 :做者 字段的字段值爲 "王國維" 時就返回真,前面介紹過的 getf 函數,如今能夠拿來使用了,能夠用它來獲取 單條記錄 中 :做者 字段的值,也就是列表 (:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T) 中的第二個元素,語句以下:
(getf 單條記錄 :做者)
實際上等價於這條語句:
(getf (:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T) :做者)
再用一個比較函數 equal 拿它返回的值跟一個咱們輸入的包含做者名字的字符串參數進行比較,好比咱們想拿做者名字是 "王國維" 的字符串進行比較,代碼以下:
(equal (getf 單條記錄 :做者) "王國維")
那麼完整的代碼以下:
CL-USER> (remove-if-not #'(lambda (單條記錄) (equal (getf 單條記錄 :做者) "王國維")) *書籍數據庫*) ((:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER>
咱們能夠把上面這段代碼包裝一下,作成一個能夠用 做者 做爲輸入參數的的函數裏,以下:
(defun 用做者名查找 (查找字符串-做者) (remove-if-not #'(lambda (單條記錄) (equal (getf 單條記錄 :做者) 查找字符串-做者)) *書籍數據庫*))
這個函數涉及到 Common Lisp 的一個聽說是比較有趣的特性---閉包,不過奇怪的是我從沒以爲閉包有什麼特別。。。
這樣咱們完成一個能夠經過 做者 來查詢的函數,可是極可能你還會須要經過 價格 查詢、經過 書名 查詢、經過任意一個字段查詢,怎麼辦呢?難道要把這些函數都寫一遍嗎?感受好像有不少重複代碼,先寫一個經過書名查詢的函數看看:
(defun 用書名查找 (查找字符串-書名) (remove-if-not #'(lambda (單條記錄) (equal (getf 單條記錄 :書名) 查找字符串-書名)) *書籍數據庫*))
果真不出所料,整個函數體中只有匿名函數體中這條語句 (getf 單條記錄 :書名) 裏的 :書名 跟上一個函數的 :做者 不同。
那麼很顯然,針對每一個字段編寫這麼一個函數是一種比較愚蠢的行爲,咱們如今還只有 4 個字段,編 4 個基本相似的查找函數還勉強行得通,可若是未來擴展到 100 個字段怎麼辦? 難道要編 100 個極其類似的查找函數?
這種瘋狂、低效的行爲咱們是毫不提倡的,那麼就想辦法把這個功能再抽象一下,用一種通用的方法來實現,由於上述兩段代碼惟一的區別在匿名函數,咱們能夠把匿名函數抽象出來。
假設用 根據?查找函數 這個名稱來代替匿名函數,其中的 ? 能夠換成 書名、做者 乃至任何一個字段,咱們再定義一個通用的 查找 函數,它以函數 *根據?查找函數 爲參數,僞碼以下:
(defun 查找 (根據?查找函數) (remove-if-not 根據?查找函數 *書籍數據庫*))
對比一下,就會發現這個通用的函數 查找 的結構跟前面的函數 用做者名查找 和 用書名查找 基本同樣,惟一不一樣的地方就是用 根據?查找函數 替換了原來的匿名函數 #'(lambda (單條記錄) (equal (getf 單條記錄 :書名) 查找字符串-書名)) 。
爲何 remove-if-not 的第一個參數 根據?查找函數 沒有使用 #' ?由於對於函數 remove-if-not 來講,它不但願獲得一個固定的名爲 根據?查找函數 的函數,實際上它也沒法獲得這個函數,由於這個函數不存在,符號 根據?查找函數 只是一個變量,是一個用於參數傳遞的變量,這個變量先接收一個匿名函數保存起來,再把它保存的匿名函數做爲函數 查找 的實參傳遞給函數 查找 的相應位置。
當你真正執行函數 查找 時,你仍是會在它的輸入參數(也就是那個匿名函數)前加上 #' ,以下:
CL-USER> (查找 #'(lambda (單條記錄) (equal (getf 單條記錄 :做者) "王國維"))) ((:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER>
不過不加 #' 也能夠,以下:
CL-USER> (查找 (lambda (單條記錄) (equal (getf 單條記錄 :做者) "王國維"))) ((:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER>
這是由於 Common Lisp 對於匿名函數 lambda 的處理機制如此,不帶 #' 的 lambda 匿名函數,當它出如今一個會被求值的上下文時,會被展開成一個帶 #' 的 lambda 匿名函數,好比 (lambda () 42) 會被展開成 #'(lambda () 42) 。
想要深究的朋友能夠嘗試一下這個【錯誤試驗】
感受這種調用方法看起來有些不太清爽,長長的一串,咱們再用一個函數把匿名函數也包裝一下,以下:
(defun 選擇器-選做者 (做者) #'(lambda (單條記錄) (equal (getf 單條記錄 :做者) 做者)))
如今對函數 查找 的調用看起來清爽多了,以下:
CL-USER> (查找 (選擇器-選做者 "王國維")) ((:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER>
那麼接下來就要定義其餘的選擇器函數了,好比 選擇器-書名、選擇器-價格 等等,可這些工做一樣會有大量重複代碼,因而咱們但願繼續抽象,把共同的部分提煉出來,乾脆搞一個通用的選擇器函數生成器,它能夠根據傳遞的參數,自動生成可用於不一樣字段甚至字段組合的選擇器函數。
這個選擇器函數生成器須要咱們增長一點關於函數的知識儲備:關鍵字形參 &key:
目前咱們所使用過的函數都是比較簡單的形參列表,形參和實參一一對應地進行綁定,函數定義了幾個形參,在調用時就必須輸入幾個實參,不然就會報錯。可是不少時候,咱們都但願函數可以提供一種靈活的參數輸入方式,好比能夠指定對特定參數的輸入,同時有些參數若是沒有輸入就由函數自動設置一個默認值。
關鍵字形參 &key 能夠實現上述這些需求,它與普通形參的惟一區別就是在形參列表開始處有一個 &key ,以下:
(defun 示例函數 (&key a b c ) (list a b c))
執行效果以下:
CL-USER> (defun 示例函數 (&key a b c ) (list a b c)) 示例函數 CL-USER> (示例函數 :a 1 :b 2 :c 3) (1 2 3) CL-USER> (示例函數 :c 3 :b 2 :a 1) (1 2 3) CL-USER> (示例函數 :c 3 :a 1) (1 NIL 3) CL-USER> (示例函數 ) (NIL NIL NIL) CL-USER>
還能夠判斷某個形參的值是從實參傳進去的仍是由函數本身指定的
。。。。。
好,知識儲備更新完畢,如今繼續研究咱們的通用選擇器函數生成器,咱們給它起個名字就叫 篩選條件 ,它應該能夠接受對應於咱們的書籍記錄字段的 4 個關鍵字形參,而後生成一個選擇器函數,咱們但願是這樣的形式:
(查找 (篩選條件 :做者 "王國維")) (查找 (篩選條件 :書名 "人間詞話"))
具體的代碼以下:
(defun 篩選條件 (&key 書名 做者 價格 (是否有電子版 nil 是否有電子版-p)) #'(lambda (單條記錄) (and (if 書名 (equal (getf 單條記錄 :書名) 書名) t) (if 做者 (equal (getf 單條記錄 :做者) 做者) t) (if 價格 (equal (getf 單條記錄 :價格) 價格) t) (if 是否有電子版-p (equal (getf 單條記錄 :是否有電子版) 是否有電子版) t))))
這個函數根據你輸入的參數來構造匿名函數,首先判斷某個參數是否有輸入,若是有就生成該字段的選擇器函數,若是沒有就不生成該字段的選擇器函數,並且不管是否的返回一個匿名函數,匿名函數的返回是。
仔細分析就會發現函數 篩選條件 中有兩種參數,一種是須要顯式輸入的 關鍵字形參: 書名、做者等查詢條件,這些參數由函數 篩選條件 接收,而後傳遞給其內部的匿名函數 Lambda 對應的位置,另外一種就是匿名函數 lambda 的形參 單條記錄,在這裏看不到有明顯的傳遞,由於函數 查找 包裝了函數 remove-if-not,層次關係以下:
(remove-if-not (篩選條件 (關鍵字形參) (lambda (單條記錄) 匿名函數體)) *書籍數據庫*)
因此形參 單條記錄 實際是經過函數 remove-if-not 提供的變量---列表 *書籍數據庫* 傳遞的,它會在列表的元素上挨個循環,也就是說會把列表中的全部元素依次綁定到 單條記錄 上,因此看起來這個參數的傳遞不是很直觀
執行效果以下:
一、根據做者查找: CL-USER> (查找 (篩選條件 :做者 "王國維")) ((:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER> 二、根據做者和書名組合查找: CL-USER> (查找 (篩選條件 :做者 "王國維" :書名 "人間詞話")) ((:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER> 三、根據做者和是否有電子版組合查找: CL-USER> (查找 (篩選條件 :做者 "王國維" :是否有電子版 T)) ((:書名 "人間詞話" :做者 "王國維" :價格 100 :是否有電子版 T)) CL-USER> 四、若是不輸入任何篩選條件,會是什麼結果呢?也就是這樣: (查找 (篩選條件 )) 若是你能自行在頭腦裏把這個結果推出來,那就說明你對這個函數的邏輯真正理解了,也真正明白 remove-if-not 函數的處理邏輯,一時搞不清楚也不要緊,到環境裏跑一下程序就清楚了。
你能夠本身試試在 remove-if-not 後面使用 #'根據?查找函數 ,而後在調用函數 查找 時不在它的輸入參數前加 #' ,看看結果如何,試錯代碼以下:
用於試錯的函數定義: (defun 查找 (根據?查找函數) (remove-if-not #'根據?查找函數 *書籍數據庫*))
實際上若是這樣定義這個函數,那個做爲實參的匿名函數是沒辦法正確傳遞到咱們所指望的位置上的,你試着先編譯一下新定義,再帶一個匿名函數實參執行一次 查找 函數就知道了。
用於試錯的函數調用: (查找 (lambda (單條記錄) (equal (getf 單條記錄 :做者) "王國維"))) (查找 #'(lambda (單條記錄) (equal (getf 單條記錄 :做者) "王國維"))) 在我使用的 Clozure CL 環境下,這兩種調用方式都沒法正確傳遞實參。
通過持續的努力,咱們得到了至關完美的通用函數 查看 和 篩選條件 ,如今已經具有編寫一個全部數據庫都須要的重要函數--更新 函數了,在關係數據庫查詢語言 SQL 中,它通常叫 update ,更新 函數在數據庫中的做用很是大,有了它咱們能夠方便地修改部分數據,而不須要把錯誤的記錄先刪除再從新輸入。
由於有了前面準備的基礎,咱們能夠很迅速地整理出 更新 函數的思路:
使用一個經過參數傳遞的選擇器函數來選取須要更新的記錄,再使用關鍵字形參來指定須要改變的值。
代碼以下:
(defun 更新記錄 (根據?查找函數 &key 書名 做者 價格 (是否有電子版 nil 是否有電子版-p)) (setf *書籍數據庫* (mapcar #'(lambda (單條記錄) (when (funcall 根據?查找函數 單條記錄) (if 書名 (setf (getf 單條記錄 :書名) 書名)) (if 做者 (setf (getf 單條記錄 :做者) 做者)) (if 價格 (setf (getf 單條記錄 :價格) 價格)) (if 是否有電子版-p (setf (getf 單條記錄 :是否有電子版) 是否有電子版))) 單條記錄) *書籍數據庫*)))
這段代碼的主體部分由這兩個函數:setf 和 mapcar 的具體代碼組成,簡單說就是先用 mapcar 在原來的數據庫變量 *書籍數據庫* 的基礎上生成一個結構徹底相同,可是部分字段值發生更新的新列表做爲返回值,僞碼以下:
(mapcar #'把後面的列表參數中的指定字段按照指定值進行更新 *書籍數據庫*)==>新列表
而後再使用賦值函數 setf 把這個新列表賦值給全局變量 *書籍數據庫* ,僞碼以下:
(setf *書籍數據庫* mapcar返回的新列表)
注意函數 mapcar 使用的那個匿名函數 lambda ,它執行的操做實際也是一個小循環,每次從全局變量 *書籍數據庫* 列表中取一條記錄,執行這條語句:
(when (funcall 根據?查找函數 單條記錄) 。。。
若是記錄有效則返回真值(由於咱們在這裏調用的 根據?查找函數 函數是 篩選條件,因此也就是根據你輸入的篩選參數針對每條 單條記錄 進行對照查找),繼續執行內部的判斷、賦值語句。
實例以下:若是用戶在 篩選條件 函數中輸入了關鍵字形參 :書名 的實參值 "人間詞話" 做爲篩選參數,而後在後面更新字段的關鍵字形參中輸入 :價格 的實參值 24,也就是說用戶輸入的篩選條件爲 :書名 = "人間詞話",更新字段爲 :價格 ,更新值爲 24, 形如:
(更新記錄 (篩選條件 :書名 "人間詞話") :價格 24)
則根據用戶的輸入值更新該條記錄中對應的字段值。
大體說一下 mapcar 這個函數,這是一個操做列表的函數,它的返回結果也是一個列表,它的第一個參數是一個函數,後續的參數都是列表。
它利用第一個函數參數對指定列表的對應元素進行操做,若是後續參數是一個數字列表,它能夠給列表中每一個元素加個1,而後返回新列表:
CL-USER> (mapcar #'(lambda (x) (+ 1 x)) (list 1 2 3 4 5 6 7 8 9)) (2 3 4 5 6 7 8 9 10) CL-USER>
若是後續參數是兩個長度相同的列表,它也能夠把這兩個長度相同的數字列表中的每一個元素相加求和,把全部的和做爲新列表中的元素,而後返回新列表:
CL-USER> (mapcar #'+ (list 1 2 3 4 5 6 7 8 9) (list 2 3 4 5 6 7 8 9 10)) (3 5 7 9 11 13 15 17 19) CL-USER>
看了這兩個例子你們想必都清楚了:mapcar 的第一個參數是一個函數,後續的參數類型由這個被調用的函數決定。
【小做業】試着用 **mapcar** 把一個數字列表中全部元素求和,而後返回和值:
開始編寫這個程序時咱們輸入的第一條關於王國維的 《人間詞話》的記錄,那個價格 100 實際上是不確切的,如今咱們查到了正確的價格,是 24 ,但願能修改一下數據庫,正好試試咱們剛寫好的 更新 函數,執行效果以下:
CL-USER> (更新 (篩選條件 :做者 "王國維") :價格 24) ((:書名 "血色黃昏" :做者 "老鬼" :價格 "25" :是否有電子版 "n") (:書名 "難忘的書與插圖" :做者 "汪家明" :價格 38 :是否有電子版 T) (:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T) (:書名 "人間詞話" :做者 "王國維" :價格 24 :是否有電子版 T)) CL-USER>
雖然執行以後的返回結果已經顯示成功修改了數據庫,不過咱們仍是能夠再用 查找 函數單獨看一下:
CL-USER> (查找 (篩選條件 :做者 "王國維")) ((:書名 "人間詞話" :做者 "王國維" :價格 24 :是否有電子版 T)) CL-USER>
顯示沒問題,咱們已經成功修改了這條記錄的價格字段的內容!再次慶祝一下!
順便再寫一個刪除記錄的函數 刪除記錄
(defun 刪除記錄 (根據?查找函數) (setf *書籍數據庫* (remove-if 根據?查找函數 *書籍數據庫*)))
這裏使用了一個跟函數 remove-if-not 形式相似的一個函數 remove-if ,在它所返回的列表中,全部匹配謂詞的元素都被刪除。
有的朋友可能會拿這個 刪除記錄 函數跟上一個 更新 函數進行比較,發現 刪除記錄 函數的形參中沒有那些關鍵字形參,分別以下:
(defun 更新記錄 (根據?查找函數 &key 書名 做者 價格 (是否有電子版 nil 是否有電子版-p)) (defun 刪除記錄 (根據?查找函數)
比較一下這兩個函數的實際調用代碼就清楚了:
(更新記錄 (篩選條件 :做者 "王國維") :價格 24) (刪除記錄 (篩選條件 :做者 "王國維"))
前者須要輸入兩次關鍵字形參,第一次是爲 篩選條件 函數準備的,用來篩選出符合條件的記錄,第二次是爲更新內容準備的,用來取代記錄中原來的值。
後者只須要輸入一次關鍵字形參,並且被包裝在 篩選條件 函數裏了,不須要在函數 刪除記錄 的定義中出現,由於你調用 篩選條件 時天然會輸入它須要的關鍵字形參。
下意識地以爲這個 刪除記錄 的函數是一個危險的函數,尤爲當它的 篩選條件 不帶任何參數的時候,因此咱們在試驗這個函數以前,先把咱們辛辛苦苦輸入的數據庫保存一下,就用咱們前面完成的函數 保存數據庫 ,代碼以下:
CL-USER> (保存數據庫 "~/ecode/markdown-doc/book-db.txt") ((:書名 "血色黃昏" :做者 "老鬼" :價格 "25" :是否有電子版 "n") (:書名 "難忘的書與插圖" :做者 "汪家明" :價格 38 :是否有電子版 T) (:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T) (:書名 "人間詞話" :做者 "王國維" :價格 24 :是否有電子版 T)) CL-USER>
作好了萬全的準備,開始試驗新的危險函數,先不帶任何篩選條件試試,以下:
CL-USER> (刪除記錄 (篩選條件)) NIL CL-USER> *書籍數據庫* NIL CL-USER>
果真預感成真,內存裏的全局變量 *書籍數據庫* 的內容被完全清空了,好在咱們有文件備份,先把它恢復,以下:
CL-USER> (加載數據庫 "~/ecode/markdown-doc/book-db.txt") ((:書名 "血色黃昏" :做者 "老鬼" :價格 "25" :是否有電子版 "n") (:書名 "難忘的書與插圖" :做者 "汪家明" :價格 38 :是否有電子版 T) (:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T) (:書名 "人間詞話" :做者 "王國維" :價格 24 :是否有電子版 T)) CL-USER> *書籍數據庫* ((:書名 "血色黃昏" :做者 "老鬼" :價格 "25" :是否有電子版 "n") (:書名 "難忘的書與插圖" :做者 "汪家明" :價格 38 :是否有電子版 T) (:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T) (:書名 "人間詞話" :做者 "王國維" :價格 24 :是否有電子版 T)) CL-USER>
很是好,數據庫信息又所有恢復了,此次再嘗試一下帶篩選條件刪除記錄,就刪除 :做者 是 "王國維" 的記錄,以下:
CL-USER> (刪除記錄 (篩選條件 :做者 "王國維")) ((:書名 "血色黃昏" :做者 "老鬼" :價格 "25" :是否有電子版 "n") (:書名 "難忘的書與插圖" :做者 "汪家明" :價格 38 :是否有電子版 T) (:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T)) CL-USER> (查找 (篩選條件 :做者 "王國維")) NIL CL-USER>
很好,乾淨利落地刪掉了這條記錄,函數的基本測試經過,收工,準備下一節,即將登場的但是 Common Lisp 的一個很是重要的特性---宏!
太激動人心了,終於寫到最後一節,實際上咱們這個小型的藏書數據庫程序已經基本完成了,在正式開始本節內容前先把咱們寫過的代碼作一個簡單的回顧,所有的代碼以下:
(defun 創建書籍信息 (書名 做者 價格 是否有電子版) (list :書名 書名 :做者 做者 :價格 價格 :是否有電子版 是否有電子版)) (defvar *書籍數據庫* nil) (defun 增長記錄 (書籍信息) (push 書籍信息 *書籍數據庫*)) (defun 轉儲顯示 () (dolist (單條書籍記錄 *書籍數據庫*) (format t "~{~a: ~20t~a~%~}~%" 單條書籍記錄))) (defun 提示輸入 (提示信息) (format *query-io* "~a: " 提示信息) (force-output *query-io*) (read-line *query-io*)) (defun 提示書籍信息-舊版 () (創建書籍信息 (提示輸入 "書名") (提示輸入 "做者") (提示輸入 "價格") (提示輸入 "是否有電子版[y/n]"))) (defun 提示書籍信息 () (創建書籍信息 (提示輸入 "書名") (提示輸入 "做者") (or (parse-integer (提示輸入 "價格") :junk-allowed t) 0) (y-or-n-p "是否有電子版[y/n]: "))) (defun 批量輸入 () (loop (增長記錄 (提示書籍信息)) (if (not (y-or-n-p "還要繼續輸入下一本書籍的信息嗎?[y/n]: ")) (return)))) (defun 保存數據庫 (帶路徑的保存文件名) (with-open-file (文件綁定變量 帶路徑的保存文件名 :direction :output :if-exists :supersede) (with-standard-io-syntax (print *書籍數據庫* 文件綁定變量)))) (defun 加載數據庫 (帶路徑的加載文件名) (with-open-file (文件綁定變量 帶路徑的加載文件名) (with-standard-io-syntax (setf *書籍數據庫* (read 文件綁定變量))))) (defun 用做者名查找 (做者名) (remove-if-not #'(lambda (單條記錄) (equal (getf 單條記錄 :做者) 做者名)) *書籍數據庫*)) (defun 查找 (根據?查找函數) (remove-if-not 根據?查找函數 *書籍數據庫*)) (defun 選擇器-選做者 (做者) #'(lambda (單條記錄) (equal (getf 單條記錄 :做者) 做者))) (defun 篩選條件 (&key 書名 做者 價格 (是否有電子版 nil 是否有電子版-p)) #'(lambda (單條記錄) (and (if 書名 (equal (getf 單條記錄 :書名) 書名) t) (if 做者 (equal (getf 單條記錄 :做者) 做者) t) (if 價格 (equal (getf 單條記錄 :價格) 價格) t) (if 是否有電子版-p (equal (getf 單條記錄 :是否有電子版) 是否有電子版) t)))) (defun 更新記錄 (根據?查找函數 &key 書名 做者 價格 (是否有電子版 nil 是否有電子版-p)) (setf *書籍數據庫* (mapcar #'(lambda (單條記錄) (when (funcall 根據?查找函數 單條記錄) (if 書名 (setf (getf 單條記錄 :書名) 書名)) (if 做者 (setf (getf 單條記錄 :做者) 做者)) (if 價格 (setf (getf 單條記錄 :價格) 價格)) (if 是否有電子版-p (setf (getf 單條記錄 :是否有電子版) 是否有電子版))) 單條記錄) *書籍數據庫*))) (defun 刪除記錄 (根據?查找函數) (setf *書籍數據庫* (remove-if 根據?查找函數 *書籍數據庫*)))
不知不覺中已經寫了這麼多代碼,真有成就感啊!
原型系統已經完成了,如今就在原型系統的基礎上針對咱們已經完成的程序作進一步的分析和優化:
前面在寫函數 篩選條件 時,咱們爲了不每針對一個字段都寫一個對應的選擇器函數而作了一些有益的抽象,寫了一個選擇器生成器函數 篩選條件 ,避免了必定程度的代碼重複,可是在 篩選條件 的代碼中實際上仍是不可避免地出現很多重複,咱們必須爲全部打算列爲篩選條件的字段都寫一條相似的語句放在 篩選條件 的函數體中,以下:
(if 書名 (equal (getf 單條記錄 :書名) 書名) t)
若是有 100 個準備列爲篩選條件的字段就須要寫出 100 條這樣的語句,以下:
(if 字段1 (equal (getf 單條記錄 :字段1) 字段1) t) (if 字段2 (equal (getf 單條記錄 :字段2) 字段2) t) (if 字段3 (equal (getf 單條記錄 :字段3) 字段3) t) (if 字段4 (equal (getf 單條記錄 :字段4) 字段4) t) 。。。。。 (if 字段100 (equal (getf 單條記錄 :字段100) 字段100) t)
是否是發現咱們前面所作的抽象還不是很完全?這樣的代碼不只會形成重複,並且在編譯以後的執行代碼中會產生多條無用的分支判斷---你寫多少個 if 它就會生成多少個分支判斷,哪怕你最終調用時只帶一個篩選參數,它也會把其他 99 條分支判斷一一遍歷。
也就是說咱們花了太多力氣去檢查用戶是否輸入某個關鍵字形參。
這種無用的分支判斷帶來的不只是代碼的重複,更有性能上的損失,固然體現咱們這個小程序上可能先後性能差別很小,不過咱們一下子能夠稍微度量一下,Common Lisp 提供了簡單的性能分析函數 time 能夠用來作這種對比,真正對性能感興趣的朋友也能夠用不一樣方式試着寫一個 100 個字段的數據庫,比較一下。
言歸正傳,如今開始考慮如何把 篩選條件 作得更抽象一些,只生成咱們實際執行的代碼,根本不去生成那些可能執行、可是沒有執行的代碼。
讓咱們從用戶調用函數 篩選條件 時輸入的調用形式入手看看,用戶可能會輸入如形式的調用代碼:
(查找 (篩選條件 :做者 "王國維" :是否有電子版 T))
咱們目前已經實現的代碼是這樣的:
(查找 #'(lambda (單條記錄) (and (if 書名 (equal (getf 單條記錄 :書名) 書名) t) (if 做者 (equal (getf 單條記錄 :做者) 做者) t) (if 價格 (equal (getf 單條記錄 :價格) 價格) t) (if 是否有電子版-p (equal (getf 單條記錄 :是否有電子版) 是否有電子版) t))))
可是咱們實際只須要執行這樣的代碼便可:
(查找 #'(lambda(單條記錄) (and (equal (getf 單條記錄 :書名) 書名) (equal (getf 單條記錄 :是否有電子版) t))))
對比發現,後者比前者少了 4 條 if 判斷,並且代碼的處理邏輯看起來也更清晰了。
很好,咱們但願每次都能根據用戶輸入的篩選字段來生成必要的代碼,而不是把全部的可能性都一一列舉,而後傻乎乎地一個分支一個分支跑一遍。
這個優化目標若是在 C 語言裏提出,我以爲實現起來會比較困難,可能爲此編寫的輔助代碼都要大大超過咱們整個程序了,沒準你爲此增長的輔助代碼能夠編一個小型專用編譯器出來了---也可能由於我本身的 C 語言水平比較有限,反正我暫時想不出什麼既簡單又有效的 C 算法,固然這麼比較可能確實對 C 不太公平,C++卻是能夠考慮考慮。
注意了,這裏 Common Lisp 的一個很是很是重要的特性終於在萬衆矚目中登場了--- 宏 Macro,絕不誇張地說,我學習 Common Lisp 有一多半的緣由就是由於它的宏,強大到逆天的能力!
什麼叫強大到逆天的能力?咱們知道,計算機程序語言中,天大地大,規則最大,全部的程序語言都要服從它們的語法規則,不然編譯器就直接把你咔嚓掉了,根本沒機會運行,也無法運行。
可是 Common Lisp 卻不同,由於它有 宏 ,Common Lisp 的 宏 賦予程序員改寫規則的能力,全部的 Common Lisp 程序員均可以按照本身的想法去創造本身的規則!就比如世間萬物都要進入生死輪迴,你卻掌握了生死簿,能夠逆天改命!
其實說實話,有些人真要用匯編或者 C 去實現這樣的功能,也不是不可能,只不過沒那麼方便而已,並且當你真的成功了,你會發現,你本身寫了一個 Common Lisp 的新實現 :)
寫到這裏,你們能夠把前面的 8 節內容都看作是專門爲這一節而作的鋪墊,我也會盡可能用我本身的理解來說述 宏 這一利器,我我的的見解:
對於初學者來講,Common Lisp 的其餘特性能夠暫時放着,慢慢去熟悉,可是 宏 必定要從開始就理解、就學會,而後再在不斷的編程實踐中去運用,這樣纔會真正改變你的編程思惟!
咱們知道,編程語言的發展,其實就是抽象程度被不斷提高的過程,從機器語言到彙編語言還只是簡單的從機器指令到助記符的對應,可是很快宏彙編就開始提出抽象,各類高級語言分別提供各類不一樣角度、不一樣層次的抽象能力。
所謂的抽象就是提取那些共性的東西,而後用一種通用的形式去表述,Common Lisp 中的 宏 機制的本質也是如此,因此咱們也沒有必要把它看得有多麼艱難,被它嚇住,只要是程序中發現有共性的代碼,均可以以各類形式抽象成 宏,最簡單的就是內容重複的代碼,這個很好判斷,咱們此次打算優化的內容就是這種類型的代碼。
很容易看出,咱們的 篩選條件 函數中最多的重複就是這種代碼,以下:
(equal (getf 單條記錄 :書名) 書名)
抽象一下就是:
(equal (getf 單條記錄 字段名) 字段值)
先作成最簡單的抽象---函數化,咱們把它編寫成一個根據輸入字段名和字段值返回表達式的函數,由於表達式自己只是列表,所以能夠先構思成這樣的僞碼:
(defun 域值->表達式 (域 值) (list equal (list getf 單條記錄 域) 值)
說明一下,這個定義使用的語法是錯誤的,由於 Common Lisp 遇到 域、值 這種符號形式沒有出如今列表首位時會去求值,這個沒問題,由於你在實際調用時會把實際的參數值傳給 域 和 值 這兩個形參,可是對於列表中出現的其餘相似的符號形式,如 equal、getf、單條記錄,它也會去求值,這就麻煩了,立刻就會出錯,不信你就把這段代碼拷貝到 REPL 中去執行一下,看看結果如何。
不過咱們在前面的基本概念中學過:防止 Common Lisp 對一個符號求值的辦法就是在符號前面加一個單引號 ' 因此真正行得通的代碼以下:
(defun 域值->表達式 (域 值) (list 'equal (list 'getf '單條記錄 域) 值)
通常來講,寫這種代碼,只有函數的形參會但願被求值,其餘的符號都不但願會求值,也就是說除了形參,其餘符號都須要加一個單引號,是否是以爲很麻煩?
還好咱們還有一種反過來的方法---先設置對整個表達式不求值,而後再設置對少數幾個符號求值,這就是反引號 `(鍵盤位置:ESC鍵下方,TAB鍵上方,數字鍵1的左方)的做用,在一個表達式前面放一個反引號能夠避免對整個表達式求值,在表達式中的子表達式前放一個逗號 , 能夠只讓該子表達式求值,所以能夠寫成更好的形式,把 list 函數也去掉了,以下:
(defun 域值->表達式 (域 值) `(equal (getf 單條記錄 ,域) ,值)
執行效果以下:
CL-USER> (域值->表達式 :做者 "王國維") (EQUAL (GETF 單條記錄 :做者) "王國維") CL-USER>
很好,跟咱們設想的如出一轍,不過有個小問題,實際調用 篩選條件 函數時輸入的形參可能不止這一對,因此咱們這裏須要一個函數,它可以從一個列表中成對地提取元素,分別做爲 域 和 值 來使用,而且須要收集在每對參數上調用 域值->表達式 函數生成的結果,最後再把這些結果用一個 and 函數封裝起來,這樣就實現了對 篩選條件 函數的全面改寫。
實現剛纔提到的這些功能需須要使用一點新知識 loop 宏,先使用再解釋,代碼以下:
(defun 域值->列表 (域值參數對列表) (loop while 域值參數對列表 collecting (域值->表達式 (pop 域值參數對列表) (pop 域值參數對列表))))
執行效果以下:
CL-USER> (域值->列表 '(:做者 :書名)) ((EQUAL (GETF 單條記錄 :做者) :書名)) CL-USER> (域值->列表 '(:做者 "王國維" :書名 "人間詞話")) ((EQUAL (GETF 單條記錄 :做者) "王國維") (EQUAL (GETF 單條記錄 :書名) "人間詞話")) CL-USER>
很是好,距離咱們的目標又近了一步,如今要作的就是把函數 域值->列表 返回的列表用 and 函數封裝起來,具體來講就是把它構造出來的全部 equal 語句都用 and 組裝起來,最後再放入一個匿名函數中,代碼以下:
(defmacro 篩選條件 (&rest 域值參數對列表) `#'(lambda (單條記錄) (and ,@(域值->列表 域值參數對列表))))
爲避免跟前面定義過的同名函數發生衝突,建議在編譯前先把前面的 篩選條件 函數更名爲 篩選條件-函數版本
這裏初步解釋一下 Common Lisp 的 宏 的一些基礎知識---憑藉這些基礎知識能夠實現很是強悍的抽象功能。
Common Lisp 的 宏 和咱們曾經學過的 C 語言的 宏 是兩個徹底不一樣的概念,後者只是一些簡單的替換。
首先是符號 ,@ 它會把緊挨着它的列表表達式的括號去掉,並把這個列表表達式的元素插入到外圍的列表中,《實用 Common Lisp 編程》中的表述爲: ,@ 會將接下來的表達式(必須求值成一個列表)的值嵌入到其外圍的列表裏,看看例子就明白了:
CL-USER> `(and ,(list 1 2 3)) (AND (1 2 3)) CL-USER> `(and ,@(list 1 2 3) 4 5) (AND 1 2 3 4 5) CL-USER>
宏 的另外一個重要基礎知識是剩餘形參符號 &rest ,當參數列表裏帶有 &rest 時,一個函數或宏能夠接受任意數量的實參,全部這些實參都將被收集到一個列表中,而且會成爲那個 &rest 後面的參數所對應的變量的值成。
還有一個必須提到的關於 宏 的函數 macroexpand-1 它會把一個宏調用展開,也就是說它執行的參數是一個合法的宏調用,包括宏名和必須的參數,它返回的結果正是這個宏將來執行時所生成的實際代碼,咱們完成一個新的 宏 定義以後,想要看看它是否能按咱們預期的方式工做,就能夠用這個函數來檢查。
執行效果以下:
CL-USER> (macroexpand-1 '(篩選條件 :做者 "王國維" :書名 "人間詞話")) #'(LAMBDA (單條記錄) (AND (EQUAL (GETF 單條記錄 :做者) "王國維") (EQUAL (GETF 單條記錄 :書名) "人間詞話"))) T CL-USER>
看起來不錯,用到咱們的 查找 函數中實際試一下, 以下:
CL-USER> (查找 (篩選條件 :做者 "王國維")) NIL CL-USER> 怎麼沒查到?難道寫錯了?看看數據庫的數據 CL-USER> *書籍數據庫* ((:書名 "血色黃昏" :做者 "老鬼" :價格 "25" :是否有電子版 "n") (:書名 "難忘的書與插圖" :做者 "汪家 明" :價格 38 :是否有電子版 T) (:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T)) CL-USER> 哦,原來那條記錄被咱們刪除了,換個篩選條件試試 CL-USER> (查找 (篩選條件 :做者 "老鬼")) ((:書名 "血色黃昏" :做者 "老鬼" :價格 "25" :是否有電子版 "n")) CL-USER>
大功告成!咱們成功地實現了根據用戶輸入的具體篩選條件動態生成執行代碼的抽象,避免了一大堆無用的分支。
不過且慢,雖然宏 篩選條件 實現了高度抽象,好像函數 更新記錄 也存在相似的問題,目前只能更新 4 個指定字段,若是字段一多就要寫不少對應的分支,讓咱們繼續沿用剛纔的分析方法,針對 更新記錄 再作一次抽象優化,這也是咱們對本身所學內容的一個嘗試和檢驗。
分析的順序依舊是自底向上,先模擬一個用戶輸入的函數調用場景,假設語句以下:
(更新 (篩選條件 :做者 "老鬼") :價格 55 :是否有電子版 T)
那麼對應的實際執行的代碼以下:
(更新記錄 (篩選條件 ) 更新字段 更新值 (setf *書籍數據庫* (mapcar #'(lambda (單條記錄) (when (funcall 篩選條件 單條記錄) (progn (setf (getf 單條記錄 :價格) 55) (setf (getf 單條記錄 :是否有電子版) T)) 單條記錄) *書籍數據庫*)))
一樣地,先抽象一個 更新域值->表達式 的輔助函數出來,以下:
(defun 更新域值->表達式 (域 值) `(setf (getf 單條記錄 ,域) ,值))
執行效果以下:
CL-USER> (更新域值->表達式 :價格 55) (SETF (GETF 單條記錄 :價格) 55) CL-USER>
很好,再抽象一個能夠處理域值參數對列表的輔助函數出來,以下:
(defun 更新域值->列表 (域值參數對列表) (loop while 域值參數對列表 collecting (更新域值->表達式 (pop 域值參數對列表) (pop 域值參數對列表))))
執行效果以下:
CL-USER> (更新域值->列表 '(:價格 55 :是否有電子版 T)) ((SETF (GETF 單條記錄 :價格) 55) (SETF (GETF 單條記錄 :是否有電子版) T)) CL-USER>
很是好,如今開始構造咱們的宏 更新記錄 ,代碼以下:
(defmacro 更新記錄 (根據?查找函數 &rest 待更新域值對列表) `(setf *書籍數據庫* (mapcar #'(lambda (單條記錄) (when (funcall ,根據?查找函數 單條記錄) (progn ,@(更新域值->列表 待更新域值對列表)) 單條記錄) *書籍數據庫*))))
執行效果以下:
CL-USER> (macroexpand-1 '(更新記錄 (篩選條件 :做者 "老鬼") :價格 55 :是否有電子版 t)) (SETF *書籍數據庫* (MAPCAR #'(LAMBDA (單條記錄) (WHEN (FUNCALL (篩選條件 :做者 "老鬼") 單條記錄) (PROGN (SETF (GETF 單條記錄 :價格) 55) (SETF (GETF 單條記錄 :是否有電子版) T)) 單條記錄)) *書籍數據庫*)) T CL-USER>
試驗一下效果如何:
CL-USER> (更新記錄 (篩選條件 :做者 "老鬼") :價格 55) ((:書名 "血色黃昏" :做者 "老鬼" :價格 55 :是否有電子版 "n") NIL NIL) CL-USER> *書籍數據庫* ((:書名 "血色黃昏" :做者 "老鬼" :價格 55 :是否有電子版 "n") NIL NIL) CL-USER>
壞了,這條記錄是更新了,可是其餘另外兩條記錄卻消失了,看來有些地方出錯了,很顯然,函數 mapcar 返回了這樣的結果
(待更新記錄 nil nil)
那麼它爲何要把其他兩條不知足篩選條件的記錄設置爲空值呢?看來問題仍是出在匿名函數,通過檢查,發現有一個括號位置搞錯了,正確的代碼應以下:
(defmacro 更新記錄 (根據?查找函數 &rest 待更新域值對列表) `(setf *書籍數據庫* (mapcar #'(lambda (單條記錄) (when (funcall ,根據?查找函數 單條記錄) (progn ,@(更新域值->列表 待更新域值對列表))) 單條記錄) *書籍數據庫*)))
再次試驗一下,先展開,檢查展開形式,一切正常:
CL-USER> (macroexpand-1 '(更新記錄 (篩選條件 :做者 "王國維") :價格 55 :是否有電子版 t)) (SETF *書籍數據庫* (MAPCAR #'(LAMBDA (單條記錄) (WHEN (FUNCALL (篩選條件 :做者 "王國維") 單條記錄) (PROGN (SETF (GETF 單條記錄 :價格) 55) (SETF (GETF 單條記錄 :是否有電子版) T))) 單條記錄) *書籍數據庫*)) T CL-USER>
再執行一次更新操做,結果以下:
CL-USER> (更新記錄 (篩選條件 :做者 "老鬼") :價格 55) ((:書名 "血色黃昏" :做者 "老鬼" :價格 55 :是否有電子版 "n") (:書名 "難忘的書與插圖" :做者 "汪家明" :價格 38 :是否有電子版 T) (:書名 "說文解字" :做者 "許慎" :價格 100 :是否有電子版 T) (:書名 "人間詞話" :做者 "王國維" :價格 24 :是否有電子版 T)) CL-USER>
此次好了,沒有任何問題,如今咱們終於能夠說大功告成了!
其實那個函數 progn 不是必須的,能夠去掉,還能夠少些括號,減小犯錯誤的概率,寫成以下的形式:
(defmacro 更新記錄 (根據?查找函數 &rest 待更新域值對列表) `(setf *書籍數據庫* (mapcar #'(lambda (單條記錄) (when (funcall ,根據?查找函數 單條記錄) ,@(更新域值->列表 待更新域值對列表)) 單條記錄) *書籍數據庫*)))
如今通過咱們再次抽象定義出來的新宏 更新記錄 ,不只消除了重複代碼,避免執行空分支的判斷,並且能夠更新任意字段,不論數據庫的字段怎麼調整,咱們的代碼都不須要作任何修改就可以適應。而這正是我不惜花費大量篇幅但願能讓初學者領略到的東西,通過最後對兩個函數 篩選條件 和 更新記錄 的重寫,咱們初步體會到 Common Lisp 語言強大的 宏 ,
【程序擴展】
有些人比較喜歡更深刻的探索,好比這段程序,通過咱們的一再優化,基本的功能已經實現了,不過還有很多方向值得去擴展,下面我列兩個出來,供感興趣的朋友研究參考:
一、對數據庫字段的擴充,能夠增長咱們一開始討論時設想到的那些字段; 二、和智能終端結合,如今的智能手機都有條碼掃描的功能,並且提供了相應的操做函數,每本書都有一個 ISBN 條碼,根據 這個條碼能夠在網絡上獲取該書的不少信息,這樣就不須要咱們一一輸入了,能減輕不少工做量;
通過上述 4 個章節的學習和實踐,,若是前述每段例程代碼你都徹底理解了,我相信做爲初學者的你,不只能夠順利啓動開發環境,學會很多提高開發、調試效率的快捷操做,並且也掌握了 Common Lisp 語言的一些基本概念和用法,以及部分高級內容---宏,應該具有繼續深刻探索 Common Lisp 其餘特性的能力了,恭喜你!
其實我也是跟你們同樣的初學者,開始只是想以教程的形式總結一下學過的內容,結果不知不覺把這個新手教程寫了這麼多,有很多細節在本身單獨看書的時候其實沒想那麼多,可是一旦開始寫教程的時候,才發現這些之前沒有想過的地方,因此我以爲初學者在學習 Common Lisp 時,若是能嘗試着把本身學到的內容以教程的形式寫出來,這樣不只能夠及時地對學過的內容進行總、複習,同時也能夠本身在試着表述的過程當中發現以往學習的疏漏,另外還可讓其餘初學者多一份參考學習的資料。
所以疏漏、錯誤在所不免,但願能其餘朋友能不吝賜教,指出謬誤之處,我覈實後會刷新版本,同時你的名字也會出如今 「貢獻者列表」 中。
一我的的力量終究是有限的,但願有更多的人共同參與進來
學過其餘編程語言的朋友,在瞭解了 Common Lisp 初步知識後,確定會有一個疑問:Common Lisp 有哪些 「標準庫函數」 ?如何去查詢?這也是我當初的一個疑問,由於 Common Lisp 不這麼叫,它的稱呼是 「擴展符號」,寫在 HyperSpec 中,由 LispWorks 維護,有在線版,也能夠下載回去慢慢查,一共有 978 個,建議初學者把它下載回來常常翻翻,由於能夠看到這些 「標準庫函數」 的源代碼,它們絕大多數也是用 Common Lisp 寫的。
HyperSpec 下載地址: http://www.lispworks.com/documentation/HyperSpec/Front/X_AllSym.htm
這裏再重複一遍最最簡單的查看 Common Lisp 的 「標準庫函數」 源代碼的辦法,固然,你得先知道函數名,在 REPL 或者在 編輯區 裏輸入函數名,而後把光標移動到函數名上面,按以下快捷鍵:
M-. Alt 鍵 和 點鍵 . Emacs就會自動把該函數或宏的源文件打開
其實 Emacs 裏還有很多快捷鍵能夠查詢某個函數相關的信息,不過我以爲那些幫助信息其實不如看代碼清楚,因此就很少介紹了,感興趣就本身去查吧。
目前的 LispBox 裏只使用了一種 Common Lisp 實現 Clozure CL 做爲編譯器,可是其餘實現也各有其獨到之處,好比 SBCL,好比 LispWorks,還有 Allegro CL,這些不一樣實如今其網站都有相關的文檔,包括了最權威的幫助信息。 各實現官網地址以下: Clozure CL: http://ccl.clozure.com/ SBCL: http://www.sbcl.org LispWorks: http://www.lispworks.com/ Allegro CL: http://www.franz.com/products/allegrocl/
若是遇到問題,首選是看文檔,看看官方的 FAQ 裏有沒有你的問題,其次是使用搜索引擎看看有沒有其餘人遇到相似問題,若是這兩步都沒有找到相關的答案,那就到 Email-List 或者論壇去提問,不過我的感受 Email-List 上高人更集中,畢竟你不可能把全部的論壇都逛遍,我有幾個問題就是在 Email-List 求助,而後獲得其餘朋友的幫助而解決的。
在 Email-List 上求助必定要詳細描述你的問題,最好同時把你搜索答案的過程和結果也描述一下,不然別人就算想幫你也無從下手,另外就是一些簡單的問題、或者本身能夠經過搜索找到答案的問題就不要在 Email-List 上提了,本身歷來不願動腦子、只知道要完整解決方案的伸手黨是最不受歡迎的。
最後就是但願全部的初學者都能養成幫助別人的習慣,高手、新手是相對的,你從一個高手那裏得到了幫助,同時有些比你學習晚的新手也能夠從你這裏獲得幫助,這樣正能量才能流通起來,最終受益的是網絡上的每一個使用者---也就是你我。
若是你認真地把這份教程從頭讀到尾,並且本身驗證了其中全部的例程,也理解了全部的代碼,那你就能夠開始更進一步的學習了,具體的建議我就不提了,只提幾個原則性的:
由於本教程做者也是一名初學者,因此本文必然會有各類錯漏,所以但願更多的初學者或高手能參與進來,共同完善此教程,此教程會放在 GitHub 上(https://github.com/FreeBlues/PwML/blob/master/Common%20Lisp%20初學者快速入門指導.md),方便你們查閱,固然有什麼建議也能夠直接在 oschina.net 上對本文提出評論,這裏專門開闢一個章節來列出參與發現錯誤的朋友,表示感謝!
以參與前後順序列出各位貢獻者,先把個人名字列出來,作個表率 :)