翻譯者:FreeBluesgit
github版本:https://github.com/FreeBlues/Land-of-lisp-CN程序員
開源中國版本:http://my.oschina.net/freeblues/blog?catalog=516771github
既然咱們已經討論過 Lisp
的一些哲學而且擁有了一個正常運行的 CLISP
環境,咱們就準備以一個簡單遊戲的形式來寫一些實際的 Lisp 代碼。算法
咱們將要編寫的第一個遊戲是能想到的最簡單的遊戲。 它是一個經典的猜數遊戲。編程
在這個遊戲中,你選擇一個從1到100的數字,接着計算機要把它猜出來。安全
接下來展現了當你選取數字23時遊戲玩起來是什麼樣子。計算機以50開始猜,而且隨着每次不停的猜想,你要輸入 (smaller) 或 (bigger) 直到計算機猜中你的數字。編程語言
>(guess-my-number) 50 >(smaller) 25 >(smaller) 12 >(bigger) 18 >(bigger) 21 >(bigger) 23
爲了建立這個遊戲,咱們須要編寫3個函數:guess-my-number,bigger 和 smaller。玩家簡單地從 REPL 調用這3個函數就能夠了。正如你在前面章節所看到的,當你啓動 CLISP (或者其餘 Lisp),REPL 將會呈如今你面前,經過它你輸入的命令能夠被讀取(read),而後被求值(evaluated),最後被打印出來(printed)。在這個例子裏,咱們要運行命令 guess-my-number,bigger 和 smaller。函數
在 Lisp 中調用函數,你要把這個函數和打算傳給這個函數的全部參數一塊兒用括號括起來。既然一部分函數不須要任何參數,咱們只要用括號把它們的名字括起來就能夠了。佈局
讓咱們考慮一下這個簡單遊戲背後的策略。簡單思考一下,咱們經過如下步驟來逐步實現:ui
1.肯定玩家數字的上限和下限(大和小)。由於範圍在1到100之間,最小的數字應該是1,最大的數字應該是100。 2.在這兩個數字之間猜一個數。 3.若是玩家說真實數字更小一些,下降上限(最大數字)。 4.若是玩家說真實數字更大一些,增長下限(最小數字)。
經過上述簡單步驟,每次猜想都把可能的數字範圍縮小一半,計算機能很快找出玩家的數字。
這種搜索算法被稱爲二分法(binary search),正如你所知,像這樣的二分法一直被用於計算機編程中。你能沿用相同的步驟,例如,高效地找到一個特定的數字,從被給定的數值的排序表中。在這個例子裏,你能夠簡單地追蹤表裏的最小行和最大行,而且很快找到正確的行,以一種相似的方式。
當玩家調用那些構成咱們遊戲的函數時,程序須要追蹤下限和上限。爲了作到這一點,咱們須要建立兩個被稱爲 *small* 和 *big* 的全局變量。
在 Lisp 中一個被定義爲全局的變量被稱爲一個頂層定義(top-level definition)。咱們可使用函數 defparameter 來建立新的頂層定義:
>(defparameter *small* 1) *small* >(defparameter *big* 1) *big*
函數名 defparameter
會帶來一點困惑,由於它實際上沒有對參數(parameter
)作任何操做。它所作的就是讓你定義一個全局變量(global variable)。
咱們發送給 defparameter 第一個參數是心變量的名字。環繞名字 *small* 和 *big* 先後的星號(*)--被暱稱爲耳套(earmuffs)--徹底是隨意和可選的。Lisp 把星號當作變量名的一部分而且忽略掉它們。Lisper 喜歡以這種方式做爲一個約定俗成的慣例爲他們的全局變量標上星號,以便它們能夠更容易和局部變量區分開來,在本章的後面將會討論這一點。
注意
儘管耳套在嚴格技術意義上來講是可選的,我仍是建議使用它們。我沒法保證你的安全,若是你把代碼貼到一個 Common Lisp 新聞組而且你的全局變量沒有帶耳套。
當你使用 defparameter 設置一個全局變量的值時,任何以前存儲在這個變量中的值都會被重寫覆蓋掉:
>(defparameter *foo* 5) FOO >*foo* 5 >(defparameter *foo* 6) FOO >*foo* 6
正如你所見,當咱們從新定義了變量 *foo* 以後,它的值變了。
另外一個能夠用來聲明全局變量的命令被稱爲 defvar ,它不會覆蓋掉一個全局變量以前的值:
> (defvar *foo* 5) FOO > *foo* 5 > (defvar *foo* 6) FOO > *foo* 5
注意
當你在其餘地方閱讀關於 Lisp 的知識時,你可能也會看到程序員們在 Common Lisp 中使用術語動態變量(dynamic variable)或特殊變量(special variable)來指一個全局變量。這是由於 Common Lisp 中的全局變量有一些特殊的能力,咱們將會在後面的章節討論這些。
跟其餘語言比起來,Lisp 中命令被調用的方式和代碼被格式化的方式有些奇怪。首先,咱們須要用括號把命令(以及命令的參數)括起來,就像 defparameter 函數同樣:
>(defparameter *small* 1) *small*
缺乏括號的話,命令不會被調用。
此外,空格和換行被徹底忽略,當 Lisp 讀入你的代碼時。這意味着你能用任何瘋狂的方式來調用這個命令,而結果不會變:
> ( defparameter *small* 1) *SMALL*
由於 Lisp 代碼能以這種靈活的方式格式化,Lisper 對於格式化命令有不少約定俗成的慣例,包括何時使用多行和縮進。在本書的代碼實例上,咱們將會大體遵循一些常見的縮排慣例。不過,相對於討論源代碼縮排規則咱們更感興趣的是編寫一些遊戲,所以本書中咱們將不會在代碼佈局上花費過多時間。
咱們的猜數遊戲經過計算機對玩家請求的響應來開始遊戲,而後請求更小或更大的猜想。爲了實現這些,咱們須要定義3個全局函數:guess-my-number,bigger 和 smaller。咱們還要定義一個名爲 start-over 的函數,用來從新開始遊戲以一個不一樣的數字。在 Common Lisp 中,用 defun
來定義函數,以下所示:
(defun function_name (參數) ...)
首先,咱們爲一個函數指明名字和參數。而後咱們接着寫組成函數處理邏輯的代碼。
咱們定義的第一個函數是 guess-my-number。這個函數使用變量 *small* 和 *big* 的值來生成一個針對玩家數字的猜想。定義以下所示:
> (defun guess-my-number () (ash (+ *small* *big*) -1)) GUESS-MY-NUMBER
在函數名字 guess-my-number 以後的空括號 () 指明這個函數不須要參數。
儘管在把片斷代碼輸入到 REPL 時不須要擔憂縮排和斷行,你必須確保把括號的位置放置正確。若是你忘掉一個後括號或者把一個括號放到了錯誤的位置上,你極可能會獲得一個錯誤。
當咱們任什麼時候候像這樣在 REPL 裏運行一段代碼時,輸入表達式的結果值將會被打印出來。Common Lisp 中的每個命令都會產生一個返回值。例如 defun
命令簡單地返回新建函數的函數名。這就是爲何咱們看到在咱們調用 defun
以後在 REPL 中函數名被鸚鵡學舌般返回給咱們。
這個函數作了什麼?正如以前討論過的,這個遊戲中計算機最好的猜想將是一個介於兩個限制之間的數字。爲了完成這一點,咱們選擇兩個限制的平均值。然而,若是平均值以一個分數結尾的話,咱們想要使用近似(near-average)數,由於咱們猜想的是完整的數字。
咱們在函數 guess-my-number 中實現這些功能經過如下處理:首先把上限值和下限值加在一塊兒,,而後使用算數移位函數 ash
,來使上限值、下限值之和減半而且截短結果。代碼 (+ *small* *big*)
把這兩個變量加起來。由於加法用另外一個函數調用, <1> ,加的結果被接着傳遞給函數 ash
。
包圍函數 ash
和函數 (+)
的括號在 Lisp 中是必需要有的。這些括號告訴 Lisp 「我想讓你立刻調用這個函數」。
內置的(build-in) Lisp 函數 ash
以二進制的方式看待一個數字,而後把它全部的二進制位(bits)同時移向左邊或右邊,丟掉在這個過程當中失去的任何位(譯者注:)。例如,十進制數字 11
用二進制表達就是 00001011
。咱們能夠向左移動這個數字裏全部的位,經過 ash
把 1
做爲第二個參數:
>(ash 11 1) 22
這樣就產生了 22
,二進制是 00010110
。咱們也能夠把全部位向右移動(去掉了最後的一位 1
)經過用 -1
做爲第二個參數:
>(ash 11 -1) 5
這樣會產生5,二進制是 00000101
。
經過在 guess-my-number
中使用函數 ash
,咱們能夠連續減半可能數字的搜索空間來快速縮小最終正確數字的範圍。正如以前提到的,這種減半處理被稱爲二分搜索
,一種在計算機編程中頗有用的技術。函數 ash
常常被用於 Lisp 中這些二分搜索
。
讓咱們看看當咱們的新函數被調用時將會發生什麼:
>(guess-my-number) 50
由於這是第一次猜想,咱們看到調用這個函數的輸出告訴咱們一切都按計劃進行:程序選擇了數字 50
,正好位於 1
和 100
的中間。
在用 Lisp 編程時,你將會寫不少函數,它們不會明確打印值到屏幕上。做爲替代,它們將會簡單地把函數體的計算值返回。例如,咱們說咱們想要一個函數僅僅返回數字 5 ,咱們能夠這樣寫:
> (defun return-five () (+ 2 3))
由於函數體裏計算的值被求值爲 5,調用 (return-five)
只會返回 5。
這就是 guess-my-number 的設計思路。咱們看到這個被計算後的結果出如今屏幕上(數字 50)不是由於函數使這個數字顯示,而是由於這是 REPL 的一個特性。
注意
若是你以前使用過其餘編程語言,你可能記得爲了讓一個值被返回不得不寫一些相似 return… 的東西。在 Lisp 中,這是沒必要要的。函數體中被計算的最終值會被自動返回
如今要寫咱們的 smaller
和 bigger
函數了。像 guess-my-number
同樣,這些都是用 defun
定義的全局函數:
> (defun smaller () (setf *big* (1- (guess-my-number))) (guess-my-number)) SMALLER > (defun bigger () (setf *small* (1+ (guess-my-number))) (guess-my-number)) BIGGER
首先,咱們使用 defun
來開始一個新全局函數 smaller
的定義。由於這個函數不帶任何參數,因此函數名後面的括號是空的 <1>。
接着,咱們使用 setf
函數來改變咱們全局變量 *big*
的值 <2>。由於咱們知道那個數字必需要比上次猜的值更小一些,最大的它如今是比猜想值要小的那個。代碼 (1- (guess-my-number))
這麼計算:首先調用函數 guess-my-number
來得到最近的猜想值,而後對這個猜想值使用函數 1-
,會從猜想值裏減去 1
。
最後,咱們想要函數 smaller
給咱們顯示一個新的猜想值。咱們經過把函數 guess-my-number
放在函數體的最後一行來實現 <3>。這一次,guess-my-number
將會使用更新過的 *big*
值,用這個值來計算下一個猜想值。咱們的函數的最終的值將會自動返回,使得咱們新的猜想值(由 guess-my-number
產生)經過函數 smaller
產生。
函數 bigger
以相同的方式工做,除了它是把 *small*
的值增長以外。終究,若是你調用函數 bigger
,你就是在說你的數字要比上一次猜想值更大,所以最小的它如今要比(就是變量 small
所對應的值)前一次猜想值更大。函數 1+
簡單地在由 guess-my-number
返回的猜想值上加 1
<4>。
能夠在這裏看到當程序猜了 56
時咱們函數的運行狀況:
> (bigger) 75 > (smaller) 62 > (smaller) 56
爲了完成咱們的遊戲,咱們將會增長函數 start-over
來從新設置咱們的全局變量:
(defun start-over () (defparameter *small* 1) (defparameter *big* 100) (guess-my-number))
正如你所見,函數 start-over
重置了變量 *small*
和 *big*
,接着再次調用函數 guess-my-number
來返回一個從新開始的遊戲。不論什麼時候只要你想啓動一個使用不一樣數字的嶄新遊戲時,你均可以調用這個函數來重置遊戲。
爲了咱們簡單的遊戲,咱們已經定義了全局變量和全局函數。然而,大多數狀況下你可能想把定義限制在一個單獨的函數中或者是一塊代碼內。這些就是被稱爲局部變量和局部函數。
定義一個局部變量。要使用命令 let
。一個 let
命令有着以下結構:
(let (variable declarations) ...body...)
在 let
命令中的第一部分是一個變量聲明的列表。在這裏咱們能夠聲明一個或多個局部變量 <1>。接着,在命令體裏(而且僅僅在這個體內),咱們能使用這些變量 <2>。這裏是關於 let
命令的一個例子:
> (let ((a 5) (b 6)) (+ab)) 11
在這個例子中,咱們分別爲變量 a
<1> 和 b
<2> 聲明瞭值 5
和 6
。這些就是咱們的變量聲明。而後,在命令 let
的體內,咱們把它們加在一塊兒 <3>,顯示出結果值 11
。
在使用一個 let
表達式時,你必須用括號把被聲明的變量所有括到一塊兒。另外,你必須把每一對變量名字和初始化變量值用另外一對括號括起來。
注意
儘管縮排和斷行是徹底隨意的,由於在一個 let 表達式裏的變量名和它們的值造成了一種簡單的表格,提倡的經驗是把被聲明的變量垂直對齊。這就是爲何在上一個例子中 b 被直接置於 a 的下方。
咱們用 flet
命令來定義局部函數。命令 flet
有着以下結構:
(flet ((function_name (arguments) ...function body...)) ...body...)
在 flet
的頂部,咱們聲明瞭一個函數(在起始兩行)。這個函數接着在這個主體內將對咱們可用 <3>。一個函數聲明包括一個函數名字,函數的參數 <1>,以及函數主體 <2>,在那裏咱們將放置函數的代碼。
這裏是一個例子:
> (flet ((f (n) (+ n 10))) (f5)) 15
在這個例子中,咱們定義了一個獨立的函數 f
,它帶着一個單獨的參數,n
<1>。函數 f
把 10
加到變量 n
上 <2>,被傳給它的。=== 接下來咱們使用數字 5
做爲參數來調用這個函數,值 15
會被返回 <3>。
跟 let
同樣,你能在 flet
的範圍內(譯者注:也就是在 flet
的頂部)定義一個或多個函數。
一個單獨的 flet
命令能被用來一次定義多個本地函數。簡單地在命令的第一部分增長多個函數聲明就能夠了:
> (flet ((f (n) (+ n 10)) (g (n) (- n 3))) (g (f 5))) 12
在這裏,咱們聲明瞭兩個函數:一個名爲 f
<1>,一個名爲 g
<2>。在 flet
的主體部分,咱們能夠當即使用這兩個函數。在這個例子裏,主體先使用參數 5
調用 f
獲得 15
,接着調用 g
來減去 3
,最終獲得的結果是 12
。
爲了使得函數名在被定義的函數中也可用(譯者注:此處是指同時定義函數能夠相互調用),咱們可使用命令 labels
。它的基本結構跟命令 flet
相同。這裏是一個例子:
> (labels ((a (n) (+ n 5)) (b (n) (+ (a n) 6))) (b 10)) 21
在這個例子裏,局部函數 a
把 5
加到一個數字上 <1>。接着,函數 b
被聲明 <2>。函數 b
調用了函數 a
,而後在結果上加 6
<3>。最終,函數 b
使用參數值 10
被調用 <4>。由於 10
加 6
加 5
等於 21
,數字 21
成爲整個表達式的最終值。當咱們想要用函數 b
調用函數 a
時 <3>,就須要咱們選擇 labels
而不是 flet
。若是咱們用了 flet
,函數 b
是不會"知道"函數 a
的。
命令 labels
容許咱們使用一個局部函數調用另外一個,同時它也容許咱們用一個函數調用它本身。這種作法在 Lisp 代碼中很常見,被稱爲遞歸(你將會在將來的章節中看到不少關於遞歸的例子)。
本章中,咱們討論了用於定義變量和函數的基本 Common Lisp 命令。一路走來,你學到了以下內容:
定義一個全局變量,使用 defparameter
命令。
定義一個全局函數,使用 defun
命令。
分別使用 let
和 flet
命令來定義局部變量和局部函數。
函數 labels
跟 flet
很類似,不過它容許函數自我調用。
調用本身的函數被稱爲遞歸函數。