前一章解釋了 Lisp 和 Lisp 程序兩者是如何由單一的原材料函數(shù),建造起來(lái)的。和任何建筑材料一樣,它的特質(zhì)既影響了我們所建造事物的種類(lèi),也影響著我們建造它們的方式。
本章描述 Lisp 世界里較常用的一類(lèi)編程方法。這些方法十分精妙,讓我們能夠嘗試編寫(xiě)更有挑戰(zhàn)的程序。
下一章將介紹一種尤其重要的編程方法,是 Lisp 讓我們得以運(yùn)用這種方法:即通過(guò)進(jìn)化的方式開(kāi)發(fā)程序,而非遵循先計(jì)劃再實(shí)現(xiàn)的老辦法。
事物的特征會(huì)受其原材料的影響。例如,一座木結(jié)構(gòu)建筑和石結(jié)構(gòu)建筑看起來(lái)就會(huì)感覺(jué)不一樣。甚至當(dāng)離得很遠(yuǎn),看不清原材料究竟是木頭還是石頭,你也可以大體說(shuō)出它是用什么造的。與之相似,Lisp 函數(shù)的特征也影響著 Lisp 程序的結(jié)構(gòu)。
函數(shù)式編程意味著利用返回值而不是副作用來(lái)寫(xiě)程序。副作用包括破壞性修改對(duì)象(例如通過(guò)rplaca) 以及變量賦值(例如通過(guò) setq)。如果副作用很少并且局部化,程序就會(huì)容易閱讀,測(cè)試和調(diào)試。Lisp 并非從一開(kāi)始就是這種風(fēng)格的,但隨著時(shí)間的推移,Lisp 和函數(shù)式編程之間的關(guān)系變得越來(lái)越密不可分。
這里有個(gè)例子,它可以說(shuō)明函數(shù)式編程和你在用其他語(yǔ)言編程時(shí)的做法到底有什么不一樣。假設(shè)由于某種原因我們想把列表里的元素順序顛倒一下。這次的函數(shù)不再顛倒參數(shù)列表中的元素順序,而是接受一個(gè)列表作為參數(shù),返回列表中的元素與之相同但是排列次序相反。
圖3.1 中的函數(shù)能對(duì)列表求逆。它把列表看作數(shù)組,按位置取反;其返回值是無(wú)意義的:
> (setq lst '(a b c))
(A B C)
> (bad-reverse lst)
NIL
> lst
(C B A)
函數(shù)如其名,bad-reverse 與好的 Lisp 風(fēng)格相去甚遠(yuǎn)。更糟糕的是,它還有其它丑陋之處:
因?yàn)槠湔9ぷ饔匈囉诟弊饔?,所以它使調(diào)用者離函數(shù)式編程的理想漸行漸遠(yuǎn)。
(defun bad-reverse (lst)
(let* ((len (length lst))
(ilimit (truncate (/ len 2))))
(do ((i 0 (1+ i))
(j (1- len) (1- j)))
((>= i ilimit))
(rotatef (nth i lst) (nth j lst)))))
圖3.1: 一個(gè)對(duì)列表求逆的函數(shù)
盡管是個(gè)反派角色, bad-reverse 仍有其可取之處:
它展示了 Common Lisp 交換兩個(gè)值的習(xí)慣用法。
rotatef 宏可以輪轉(zhuǎn)任何普通變量的值。所謂普通變量是指那些可以作為setf 第一個(gè)參數(shù)的變量。當(dāng)它只應(yīng)用于兩個(gè)參數(shù)時(shí),效果就是交換它們。
與之相對(duì),圖 3.2 中的函數(shù)能返回順序相反的列表。通過(guò)使用 good-reverse ,我們得到的返回值是顛倒順序后的列表,而原始列表原封不動(dòng):
;; 代碼 3.2: 一個(gè)返回相反順序列表的函數(shù)
> (setq lst '(a b c)
(A B C)
> (good-reverse lst)
(C B A)
> lst
(A B C)
(defun good-reverse (lst)
(labels ((rev (lst acc)
(if (null lst)
acc
(rev (cdr lst) (cons (car lst) acc)))))
(rev lst nil)))
過(guò)去常認(rèn)為可以根據(jù)外貌來(lái)判斷一個(gè)人的性格。不管這個(gè)說(shuō)法對(duì)于人來(lái)說(shuō)是否靈驗(yàn),但是對(duì)于 Lisp 來(lái)說(shuō), 這一般是可行的。函數(shù)式程序有著和命令式程序不同的外形。函數(shù)式程序的結(jié)構(gòu)完全是由表達(dá)式里參數(shù)的組合表現(xiàn)出來(lái)的,并且由于參數(shù)是縮進(jìn)的,函數(shù)式代碼看起來(lái)在縮進(jìn)方面顯得更為靈動(dòng)。函數(shù)式代碼看起來(lái)如同紙面上的行云流水; 命令式代碼則看起來(lái)堅(jiān)固駑鈍,Basic 語(yǔ)言就是一例。
即使遠(yuǎn)遠(yuǎn)的看上去,從bad- 和good-reverse 兩個(gè)函數(shù)的形狀也能分清孰優(yōu)孰劣。另外,good-reverse 不僅短些,也更加高效: 而不是因?yàn)?Common Lisp 已經(jīng)有了內(nèi)置的 reverse ,所以我們可以不用自己實(shí)現(xiàn)它。不過(guò)還是有必要簡(jiǎn)單了解一下這個(gè)函數(shù),因?yàn)樗?jīng)常能暴露出一些函數(shù)式編程中的錯(cuò)誤觀念。和 good-reverse 一樣,內(nèi)置的 reverse 通過(guò)返回值工作,而沒(méi)有修改它的參數(shù)。但學(xué)習(xí) Lisp 的人們可能會(huì)誤以為它像 bad-reverse 那樣依賴于 副作用。如果這些學(xué)習(xí)者想在程序里的某個(gè)地方顛倒一個(gè)列表的順序,他們可能會(huì)寫(xiě)
(reverse lst)
結(jié)果還很奇怪為什么函數(shù)調(diào)用沒(méi)有效果。事實(shí)上,如果我們希望利用那種函數(shù)提供的效果,就必須在調(diào)用代碼里自己處理。也就是需要把程序改成這樣
(setq lst (reverse lst))
調(diào)用 reverse 這類(lèi)操作符的本意就是取返回值,而非利用其副作用。你自己的程序也應(yīng)該用這種風(fēng)格編寫(xiě), 不僅因?yàn)樗逃械暮锰?,而是因?yàn)?,如果你不這樣寫(xiě),就等于在跟語(yǔ)言過(guò)不去。
在比較 bad-reverse 和 good-reverse 時(shí)我們還忽略了一點(diǎn),那就是 bad-reverse 里沒(méi)有 cons 。它對(duì)原始列表進(jìn)行操作,但卻不構(gòu)造新的列表。這樣是比較危險(xiǎn)的,因?yàn)橛锌赡茉诔绦虻钠渌胤竭€會(huì)用到原始列表,但為了效率,有時(shí)可能必須這樣做。為滿足這種需要,Common Lisp 還提供了一個(gè)稱為 nreverse 的求逆函數(shù)的破壞性版本。
所謂破壞性函數(shù),是指那類(lèi)能改變傳給它的參數(shù)的函數(shù)。即便如此,破壞性函數(shù)通常也通過(guò)取返回值的方式工作:你必須假定 nreverse 將會(huì)回收利用你作為參數(shù)傳給它的列表,但不能認(rèn)為它幫你在原地把原來(lái)的列表掉了個(gè)。和以前一樣,逆序后的列表只能通過(guò)返回值拿到。你仍然不能把
(nreverse lst)
寫(xiě)在函數(shù)中間,然后假定從那以后lst 的順序就是相反的了。在大多數(shù)實(shí)現(xiàn)里可以看到下面的現(xiàn)象:
> (setq lst '(a b c))
(A B C)
第 164 頁(yè)有一個(gè)很典型的例子。
> (nreverse lst)
(C B A)
> lst
(A)
要想真正求逆一個(gè)列表,你就不得不把 lst 賦給返回值,這和使用原來(lái)的 reverse 是一樣的。
如果我們知道某個(gè)函數(shù)有破壞性,這并不是說(shuō):調(diào)用它就是為了利用其副作用。危險(xiǎn)之處在于,有的破壞性函數(shù)給人留下了破壞性的印象。例如:
(nconc x y)
幾乎總是和:
(setq x (nconc x y))
效果相同。如果你寫(xiě)的代碼依賴于前一個(gè)用法,有時(shí)它可以正常工作。然而當(dāng) x 為 nil 時(shí),結(jié)果就會(huì)出人意料。
只有少數(shù) Lisp 操作符的本意就是為了副作用。一般而言,內(nèi)置操作符本來(lái)是為了調(diào)用后取返回值的。不要被sort,remove 或者 substitute 這樣的名字所誤導(dǎo)。如果你需要副作用,那就對(duì)返回值使用setq。
這個(gè)規(guī)則主張某些副作用其實(shí)是難免的。堅(jiān)持函數(shù)式的編程思想并沒(méi)有提倡杜絕副作用。而是說(shuō)除非必要最好不要有。
養(yǎng)成這個(gè)習(xí)慣可能要花些時(shí)間。不妨開(kāi)始時(shí)先盡量少用下列的操作符:
set setq setf psetf psetq incf decf push
pop pushnew rplaca rplacd rotatef shiftf
remf remprop remhash
還包括 let* ,命令式程序經(jīng)常藏匿其中。在這里要求有節(jié)制地使用這些操作符的目的只是希望倡導(dǎo)良好的Lisp 風(fēng)格,而不是想制定清規(guī)戒律。然而,僅此一項(xiàng)就可讓你受益匪淺了。
在其他語(yǔ)言里,導(dǎo)致副作用的最普遍原因就是讓一個(gè)函數(shù)返回多個(gè)值的需求。如果函數(shù)只能返回一個(gè)值,那它就不得不通過(guò)改變參數(shù)來(lái) "返回" 其余的值。幸運(yùn)的是,在 Common Lisp 里不必這樣做,因?yàn)槿魏魏瘮?shù)都可以返回多值。
舉例來(lái)說(shuō),內(nèi)置函數(shù) truncate 返回兩個(gè)值,被截?cái)嗟恼麛?shù),以及原來(lái)數(shù)字的小數(shù)部分。在典型的實(shí)現(xiàn)中,在最外層調(diào)用這個(gè)函數(shù)時(shí)兩個(gè)值都會(huì)返回:
> (truncate 26.21875)
26
0.21875
當(dāng)調(diào)用方只需要一個(gè)值時(shí),被使用的就是第一個(gè)值:
> (= (truncate 26.21875) 26)
T
通過(guò)使用 multiple-value-bind ,調(diào)用方代碼可以捕捉到兩個(gè)值。該操作符接受一個(gè)變量列表、一個(gè)調(diào)用,以及一段程序體。變量將被綁定到函數(shù)調(diào)用的對(duì)應(yīng)返回值,而這段程序體會(huì)依照綁定后的變量求值:
> (multiple-value-bind (int frac) (truncate 26.21875)
(list int frac))
(26 0.21875)
最后,為了返回多值,我們使用 values 操作符:
> (defun powers (x)
(values x (sqrt x) (expt x 2)))
POWERS
> (multiple-value-bind (base root square) (powers 4)
(list base root square))
(4 2.0 16)
一般來(lái)說(shuō),函數(shù)式編程不失為上策。對(duì)于 Lisp 來(lái)說(shuō)尤其如此,因?yàn)?Lisp 在演化過(guò)程中已經(jīng)支持了這種編程方式。諸如 reverse 和 nreverse 這樣的內(nèi)置操作符的本意就是以這種方式被使用的。其他操作符,例如 values 和 multiple-value-bind,是為了便于進(jìn)行函數(shù)式編程而專門(mén)提供的。
函數(shù)式程序代碼的用意和那些更常見(jiàn)的方法,即命令式程序相比可能顯得更加明確一些。函數(shù)式程序告訴你它想要什么;而命令式程序告訴你它要做什么。函數(shù)式程序說(shuō) "返回一個(gè)由 a 和 x 的第一個(gè)元素的平方所組成的列表:"
(defun fun (x)
(list 'a (expt (car x) 2)))
而命令式程序則會(huì)說(shuō) "取得 x 的第一個(gè)元素,把它平方,然后返回由 a 及其平方組成的列表":
(defun imp (x)
(let (y sqr)
(setq y (car x))
(setq sqr (expt y 2))
(list 'a sqr)))
Lisp 程序員有幸可以同時(shí)用這兩種方式來(lái)寫(xiě)程序。某些語(yǔ)言只適合于命令式編程 尤其是 Basic,以及大多數(shù)機(jī)器語(yǔ)言。事實(shí)上,imp 的定義和多數(shù) Lisp 編譯器從 fun 生成的機(jī)器語(yǔ)言代碼在形式上很相似。
既然編譯器能為你做,為什么還要自己寫(xiě)這樣的代碼呢?對(duì)于許多程序員來(lái)說(shuō),他們甚至從沒(méi)想過(guò)這個(gè)問(wèn)題。語(yǔ)言給我們的思想打上烙?。阂恍┝?xí)慣于命令式語(yǔ)言編程的人或許已經(jīng)開(kāi)始用命令式的術(shù)語(yǔ)思考問(wèn)題,而且會(huì)覺(jué)得寫(xiě)命令式程序比寫(xiě)函數(shù)式程序更容易。如果有一種語(yǔ)言可以助你一臂之力,這種思維定勢(shì)是值得克服的。
對(duì)于其他語(yǔ)言的同行來(lái)說(shuō),剛開(kāi)始使用 Lisp 可能像初次踏入溜冰場(chǎng)那樣。事實(shí)上在冰上比在干地面上更容易行走 如果使用溜冰鞋的話。然后你對(duì)這項(xiàng)運(yùn)動(dòng)的看法就會(huì)徹底改觀。
溜冰鞋對(duì)于冰的意義,和函數(shù)式編程對(duì) Lisp 的意義是一樣的。這兩樣?xùn)|西在一起讓你更優(yōu)雅地移動(dòng),事半功倍。但如果你已經(jīng)習(xí)慣于另一種行走模式,那么開(kāi)始的時(shí)候你就無(wú)法體會(huì)到這一點(diǎn)。把 Lisp 作為第二語(yǔ)言學(xué)習(xí)的一個(gè)障礙就是學(xué)會(huì)如何用函數(shù)式的風(fēng)格來(lái)編程。
幸運(yùn)的是,有一種把命令式程序轉(zhuǎn)換成函數(shù)式程序的訣竅。開(kāi)始時(shí)你可以把這一訣竅用到寫(xiě)好的代碼里。
不久以后你就可以預(yù)想到這個(gè)過(guò)程,一邊寫(xiě)代碼,一邊做轉(zhuǎn)換了。而在這之后一段時(shí)間,你就有能力從一開(kāi)始就用函數(shù)式的思想構(gòu)思你的程序。
這個(gè)訣竅就是認(rèn)識(shí)到命令式程序其實(shí)是一個(gè)從里到外翻過(guò)來(lái)的函數(shù)式程序。要想找出藏在命令式程序中的函數(shù)式程序,也只要把它從外到里翻一下。讓我們?cè)?imp 上實(shí)踐一下這個(gè)技術(shù)。
我們首先注意到的是初始 let 里 y 和 sqr 的創(chuàng)建。這預(yù)示著接下來(lái)會(huì)出問(wèn)題。就像運(yùn)行期的 eval ,需要未初始化變量的情況很罕見(jiàn),它們因而被看作程序染病的癥狀。這些變量就像插在程序上,用來(lái)固定的圖釘,它們被用來(lái)防止程序自己卷回到原形。
不過(guò)我們暫時(shí)先不考慮它們,直接看函數(shù)的結(jié)尾。命令式程序里最后發(fā)生的事情,也就是函數(shù)式程序在最外層發(fā)生的事情。所以第一步是抓住最后對(duì) list 的調(diào)用,然后把程序的其余部分塞進(jìn)去就好像把一件襯衫從里到外翻過(guò)來(lái)。我們繼續(xù)重復(fù)做相同的轉(zhuǎn)換,就好像我們先翻襯衫的袖子,然后再翻袖口那樣。
從結(jié)尾處開(kāi)始,我們將 sqr 替換成 (expt y 2),得到:
(list 'a (expt y 2))
然后我們將y 替換成 (car x):
(list 'a (expt (car x) 2))
現(xiàn)在我們可以把其余代碼扔掉了,因?yàn)橹耙呀?jīng)把所有內(nèi)容都填到了最后一個(gè)表達(dá)式里。在這個(gè)過(guò)程中我們擺脫了對(duì)變量 y 和 sqr 的依賴,因而也得以把 let 一起扔進(jìn)垃圾堆。
最終的結(jié)果比開(kāi)始的時(shí)候要短小,而且更好懂。在原先的代碼里,我們面對(duì)最終的表達(dá)式 (list 'a sqr), 卻無(wú)法一眼看出 sqr 的值的出處?,F(xiàn)在,返回值的來(lái)歷則像交通指示圖一樣一覽無(wú)余。
本章的這個(gè)例子很短,但這里的技術(shù)是可以推廣的。事實(shí)上,它對(duì)于大型函數(shù)應(yīng)該更有價(jià)值。即使存在一些有副作用的函數(shù),也可以把其中沒(méi)有副作用的那部分清理得干凈一些。
某些副作用比其他的更糟糕。例如,盡管下面的函數(shù)調(diào)用了 nconc
(defun qualify (expr)
(nconc (copy-list expr) (list 'maybe)))
但它沒(méi)有破壞引用透明。如果你每次都傳給它一個(gè)確定的參數(shù),那它的返回值將總是相同(equal) 的。
從調(diào)用者的角度來(lái)看,qualify 就和純函數(shù)型代碼一樣。但我們不能對(duì)bad-reverse (第19頁(yè)) 下同樣的評(píng)語(yǔ),這個(gè)函數(shù)事實(shí)上修改了它的參數(shù)。
如果不把所有副作用的有害程度都劃上等號(hào),而是有方法能把這些情況分出個(gè)高下,那樣將會(huì)對(duì)我們有很大的幫助??梢苑钦降卣f(shuō),如果一個(gè)函數(shù)修改的是其他函數(shù)都不擁有的東西,那么它就是無(wú)害的。例如, qualify 里的 nconc 就是無(wú)害的,因?yàn)樽鳛榈谝粋€(gè)參數(shù)的列表是新生成的。它不屬于任何其他函數(shù)。
通常,在我們提到擁有者關(guān)系時(shí),不能說(shuō)變量的擁有者是某某函數(shù),而應(yīng)該說(shuō)其擁有者是函數(shù)的某個(gè)調(diào)用。
盡管這里并沒(méi)有其他函數(shù)擁有變量 x :
(let ((x 0))
(defun total (y)
(incf x y)))
但一次調(diào)用的效果會(huì)在接下來(lái)的調(diào)用中看到。所以規(guī)則應(yīng)當(dāng)是:一個(gè)給定的調(diào)用 (invocation) 可以安全地修改它唯一擁有的東西。
究竟誰(shuí)是參數(shù)和返回值的擁有者 依照Lisp 的習(xí)慣,是函數(shù)的調(diào)用擁有那些作為返回值得到的對(duì)象,但它并不擁有那些作為參數(shù)傳給它的對(duì)象。凡是修改參數(shù)的函數(shù)都應(yīng)該打上"破壞性" 的標(biāo)簽,以示區(qū)別,但如果函數(shù)修改的只是返回給它們的對(duì)象,那我們沒(méi)有準(zhǔn)備什么特別的稱號(hào)給這些函數(shù)。
譬如,下面的函數(shù)就聽(tīng)從了這個(gè)提議:
(defun ok (x)
(nconc (list 'a x) (list 'c)))
但它調(diào)用的nconc 卻置若罔聞。由于nconc 拼出來(lái)的列表總是重新生成的,而沒(méi)有使用原來(lái)傳給ok 作為參數(shù)的那個(gè)列表,所以ok 總的來(lái)說(shuō)是ok 的。
如果稍微改一點(diǎn)兒,例如:
(defun not-ok (x)
(nconc (list 'a) x (list 'c)))
那么對(duì)nconc 的調(diào)用就會(huì)修改傳給not-ok 的參數(shù)了。
許多Lisp 程序沒(méi)有遵守這個(gè)慣例,至少在局部上是這樣。盡管如此,正如我們從 ok 那里看到的,局部的違背并不會(huì)讓主調(diào)函數(shù)變質(zhì)。而且那些與上述情況相符的函數(shù)仍會(huì)保留很多純函數(shù)式代碼的優(yōu)點(diǎn)。
要想寫(xiě)出真正意義上的函數(shù)式代碼,還要再加個(gè)條件。函數(shù)不能和不遵守這些規(guī)則的代碼共享對(duì)象。例如,盡管這個(gè)函數(shù)沒(méi)有副作用:
(defun anything (x)
(+ x *anything*))
但它的返回值依賴于全局變量?anything。因此,如果任何其他函數(shù)可以改變這個(gè)變量的值,那么anything 就可能返回任意值。
關(guān)于引用透明的定義見(jiàn)135頁(yè)。
要是把代碼寫(xiě)成讓每次調(diào)用都只修改它自己擁有的東西的話,那這樣的代碼就基本上就可以和純函數(shù)式代碼媲美了。從外界看來(lái),一個(gè)滿足上述所有條件的函數(shù)至少會(huì)擁有有函數(shù)式的接口:如果用同一參數(shù)調(diào)用它兩次,你應(yīng)當(dāng)會(huì)得到同樣的結(jié)果。正如下一章所展示的那樣,這也是自底向上程序設(shè)計(jì)最重要的組成部分。
破壞性的操作符還有個(gè)問(wèn)題,就是它和全局變量一樣會(huì)破壞程序的局部性。當(dāng)你寫(xiě)函數(shù)式代碼時(shí),可以集中精力:只要考慮調(diào)用正在編寫(xiě)的函數(shù)的調(diào)用方,或者被調(diào)用方就行了。要是你想要破壞性地修改某些數(shù)據(jù),這個(gè)好處就不復(fù)存在了。你修改的數(shù)據(jù)可能在任何一個(gè)地方用到。
上面的條件不能保證你能得到和純粹的函數(shù)式代碼一樣的局部性,盡管它們確實(shí)在某種程度上有所改進(jìn)。
例如,假設(shè)f 調(diào)用了g ,如下:
(defun f (x)
(let ((val (g x)))
; safe to modify val here?
))
在f 里把某些東西nconc 到val 上面安全嗎 如果g 是identity 的話就不安全:這樣我們就修改了某些原本作為參數(shù)傳給 f 本身的東西。
所以,就算要修改那些按照這個(gè)規(guī)定寫(xiě)就的程序,還是不得不看看f 之外的東西。雖然要多操心一些,但也用不著看得太多:現(xiàn)在我們不用復(fù)查程序的所有代碼,只消考慮從f 開(kāi)始的那棵子樹(shù)就行了。
推論之一是函數(shù)不該返回任何不能安全修改的東西。如此說(shuō)來(lái),就應(yīng)當(dāng)避免寫(xiě)那些返回包含引用對(duì)象的函數(shù)。如果我們這樣定義exclaim ,讓它的返回值包含一個(gè)引用列表,
(defun exclaim (expression)
(append expression '(oh my)))
那么任何后續(xù)的對(duì)返回值的破壞性修改
> (exclaim '(lions and tigers and bears))
(LIONS AND TIGERS AND BEARS OH MY)
> (nconc * '(goodness))
(LIONS AND TIGERS AND BEARS OH MY GOODNESS)
將替換函數(shù)里的列表:
> (exclaim '(fixnums and bignums and floats))
(FIXNUMS AND BIGNUMS AND FLOATS OH MY GOODNESS)
為了避免exclaim 的這個(gè)問(wèn)題,它應(yīng)該寫(xiě)成:
(defun exclaim (expression)
(append expression (list 'oh 'my)))
雖說(shuō)函數(shù)不應(yīng)返回引用列表,但是這個(gè)常理也有例外,即生成宏展開(kāi)的函數(shù)。宏展開(kāi)器可以安全地在它們的展開(kāi)式里包含引用列表,只要這些展開(kāi)式是直接送到編譯器那里的。
其他時(shí)候,還是應(yīng)該審慎地對(duì)待引用列表。除了上面的例外情況,如果發(fā)現(xiàn)用到了引用列表,很多情況,這些代碼是完全可以用類(lèi)似in (103頁(yè)) 這樣的宏來(lái)完成的。
前一章說(shuō)明了函數(shù)式的編程風(fēng)格是一種組織程序的好辦法。但它的好處還不止于此。Lisp 程序員并非完全是從美感出發(fā)才采納函數(shù)式風(fēng)格的。他們采用這種風(fēng)格是因?yàn)樗尮ぷ鞲p松。在 Lisp 的動(dòng)態(tài)環(huán)境里, 函數(shù)式程序能以非同尋常的速度寫(xiě)就,與此同時(shí),寫(xiě)出的程序也非同尋常的可靠。
在 Lisp 里調(diào)試程序相對(duì)簡(jiǎn)單。很多信息在運(yùn)行期是可見(jiàn)的,可以幫助追查錯(cuò)誤的根源。但更重要的是你
可以輕易地測(cè)試程序。你不需要編譯一個(gè)程序然后一次性測(cè)試所有東西。你可以在toplevel 循環(huán)里通過(guò)逐個(gè)地調(diào)用每個(gè)函數(shù)來(lái)測(cè)試它們。
增量測(cè)試非常有用,為了更好地利用它,Lisp 風(fēng)格也隨之改進(jìn)。用函數(shù)式風(fēng)格寫(xiě)出的程序可以逐個(gè)函數(shù)地
理解它,從讀者的觀點(diǎn)來(lái)看,這是它的主要優(yōu)點(diǎn)。此外,函數(shù)式風(fēng)格也極其適合增量測(cè)試:以這種風(fēng)格寫(xiě)出的程序可以逐個(gè)函數(shù)地進(jìn)行測(cè)試。當(dāng)一個(gè)函數(shù)既不檢查也不改變外部狀態(tài)時(shí),任何bug 都會(huì)立即現(xiàn)形。這樣,函數(shù)影響外面世界的唯一渠道是它的返回值。只要返回值是你期望的,你就完全可以信任返回它的代碼。
事實(shí)上有經(jīng)驗(yàn)的Lisp 程序員會(huì)盡量讓他們的程序易于測(cè)試:
他們?cè)噲D把副作用分離到個(gè)別函數(shù)里,以便程序中更多的部分可以寫(xiě)成純函數(shù)式風(fēng)格。
如果一個(gè)函數(shù)必須產(chǎn)生副作用,他們至少會(huì)想辦法給它設(shè)計(jì)一個(gè)函數(shù)式的接口。
一旦函數(shù)按照這種辦法寫(xiě)成,程序員們就可以用一組有代表性的情況對(duì)它測(cè)試,測(cè)試好了,就使用另一組情況測(cè)試。如果每一塊磚都各司其職,那么圍墻就會(huì)屹立不倒。
在Lisp 里,一樣可以更好地設(shè)計(jì)圍墻。先假想一下,如果談話的時(shí)候,和對(duì)方距離很遠(yuǎn),聲音的延遲甚至有一分鐘,會(huì)有什么樣的一番感受。要是換成和隔壁房間的人說(shuō)話,會(huì)有怎樣的改觀。這樣,將進(jìn)行的對(duì)話不僅僅是速度比原來(lái)快,而是一個(gè)完全不同的對(duì)話。在Lisp 中,開(kāi)發(fā)軟件就像是面對(duì)面的交流。你可以邊寫(xiě)代碼邊做測(cè)試。和對(duì)話相似,即時(shí)的回應(yīng)對(duì)于開(kāi)發(fā)來(lái)說(shuō)一樣有戲劇化的效果。你不只是把原先的程序?qū)懙酶欤菚?huì)寫(xiě)出另一種程序。
這是什么道理 當(dāng)測(cè)試更便捷時(shí),你就可以更頻繁地進(jìn)行測(cè)試。對(duì)于Lisp,和其他語(yǔ)言一樣,開(kāi)發(fā)是由編碼和測(cè)試構(gòu)成的循環(huán)往復(fù)的周期性過(guò)程。但在Lisp 的周期更短:?jiǎn)蝹€(gè)函數(shù),甚至函數(shù)的一部分都可以成為一個(gè)開(kāi)發(fā)周期。并且如果一邊寫(xiě)代碼一邊測(cè)試的話,當(dāng)錯(cuò)誤發(fā)生時(shí)你就知道該檢查哪里:應(yīng)該看看最后寫(xiě)的那部分。正如聽(tīng)起來(lái)那樣簡(jiǎn)單,這一原則極大地提高了自底向上編程的可行性。它帶來(lái)了額外的信賴感, 使得Lisp 程序員至少在一定程度上從舊式的計(jì)劃–實(shí)現(xiàn)的軟件開(kāi)發(fā)風(fēng)格中解脫了出來(lái)。
第 1.1 節(jié)強(qiáng)調(diào)了自底向上的設(shè)計(jì)是一個(gè)進(jìn)化的過(guò)程。在這個(gè)過(guò)程中,你在寫(xiě)程序的同時(shí)也就是在構(gòu)造一門(mén)語(yǔ)言。這一方法只有當(dāng)你信賴底層代碼時(shí)才可行。如果你真的想把這一層作為語(yǔ)言使用,你就必須假設(shè), 如同使用其他語(yǔ)言時(shí)那樣,任何遇到的bug 都是你程序里的bug,而不是語(yǔ)言本身的。
難道你的新抽象有能力承擔(dān)這一重任,同時(shí)還能按照新的需求隨機(jī)應(yīng)變?沒(méi)錯(cuò),在Lisp 里你可以兩不誤。
當(dāng)以函數(shù)式風(fēng)格編寫(xiě)程序,并且進(jìn)行增量測(cè)試時(shí),你可以得到隨心所欲的靈活性,加上人們認(rèn)為只有仔細(xì)計(jì)劃才能確保的可靠性。
更多建議: