《ANSI Common Lisp》- 第六章:函數(shù)【筆記】

理解函數(shù)是理解 Lisp 的關(guān)鍵之一蝇刀。概念上來說搓萧,函數(shù)是 Lisp 的核心所在毛甲。實(shí)際上呢,函數(shù)是你手邊最有用的工具之一哈扮。

6.1 全局函數(shù) (Global Functions)

謂詞?fboundp?告訴我們纬纪,是否有個(gè)函數(shù)的名字與給定的符號(hào)綁定。如果一個(gè)符號(hào)是函數(shù)的名字滑肉,則?symbol-function?會(huì)返回它:

[1]> (fboundp '+)

T

[2]> (symbol-function '+)

#<SYSTEM-FUNCTION +>

可通過?symbol-function?給函數(shù)配置某個(gè)名字

[3]> (setf (symbol-function 'add2)

? #'(lambda (x) (+ x 2)))

#<FUNCTION :LAMBDA (X) (+ X 2)>

新的全局函數(shù)可以這樣定義包各,用起來和?defun?所定義的函數(shù)一樣:

[4]> (add2 1)

3

實(shí)際上?defun?做了稍微多的工作,將某些像是

(defun add2 (x) (+ x 2))

翻譯成上述的?setf?表達(dá)式赦邻。使用?defun?讓程序看起來更美觀髓棋,并或多或少幫助了編譯器,但嚴(yán)格來說惶洲,沒有?defun?也能寫程序按声。

通過把?defun?的第一個(gè)實(shí)參變成這種形式的列表?(setf?f)?,你定義了當(dāng)?setf?第一個(gè)實(shí)參是?f?的函數(shù)調(diào)用時(shí)恬吕,所會(huì)發(fā)生的事情签则。下面這對(duì)函數(shù)把?primo?定義成car?的同義詞:

[8]> (defun primo (lst) (car lst))

PRIMO

[9]>

(defun (setf primo) (val lst)

? (setf (car lst) val))

(SETF PRIMO)

在函數(shù)名是這種形式?(setf?f)?的函數(shù)定義中,第一個(gè)實(shí)參代表新的數(shù)值铐料,而剩余的實(shí)參代表了傳給?f?的參數(shù)渐裂。

現(xiàn)在任何?primo?的?setf?,會(huì)是上面后者的函數(shù)調(diào)用:

[10]> (let ((x (list 'a 'b 'c)))

? ? (setf (primo x) 480)

? ? x)

(480 B C)

不需要為了定義?(setf?primo)?而定義?primo?钠惩,但這樣的定義通常是成對(duì)的柒凉。

由于字符串是 Lisp 表達(dá)式,沒有理由它們不能出現(xiàn)在代碼的主體篓跛。字符串本身是沒有副作用的膝捞,除非它是最后一個(gè)表達(dá)式,否則不會(huì)造成任何差別愧沟。如果讓字符串成為?defun?定義的函數(shù)主體的第一個(gè)表達(dá)式蔬咬,

[11]> (defun foo (x)

? "Implements an enhanced paradigm of diversity"

? x)

FOO

那么這個(gè)字符串會(huì)變成函數(shù)的文檔字符串(documentation string)。要取得函數(shù)的文檔字符串沐寺,可以通過調(diào)用?documentation?來取得:

[12]> (documentation 'foo 'function)

"Implements an enhanced paradigm of diversity"

6.2 局部函數(shù) (Local Functions)

通過?defun?或?symbol-function?搭配?setf?定義的函數(shù)是全局函數(shù)林艘。你可以像存取全局變量那樣,在任何地方存取它們混坞。定義局部函數(shù)也是有可能的狐援,局部函數(shù)和局部變量一樣,只在某些上下文內(nèi)可以訪問。

局部函數(shù)可以使用?labels?來定義咕村,它是一種像是給函數(shù)使用的?let?场钉。它的第一個(gè)實(shí)參是一個(gè)新局部函數(shù)的定義列表蚊俺,而不是一個(gè)變量規(guī)格說明的列表懈涛。列表中的元素為如下形式:

(name parameters . body)

而?labels?表達(dá)式剩余的部份,調(diào)用?name?就等于調(diào)用?(lambda?parameters?.?body)?泳猬。

[13]> (labels ((add10 (x) (+ x 10))

? ? ? ? ? (consa? (x) (cons 'a x)))

? ? (consa (add10 3)))

(A . 13)

labels?與?let?的類比在一個(gè)方面上被打破了批钠。由?labels?表達(dá)式所定義的局部函數(shù),可以被其他任何在此定義的函數(shù)引用得封,包括自己埋心。所以這樣定義一個(gè)遞歸的局部函數(shù)是可能的:

[14]> (labels ((len (lst)

? ? ? ? ? ? (if (null lst)

? ? ? ? ? ? ? ? 0

? ? ? ? ? ? ? ? (+ (len (cdr lst)) 1))))

? ? (len '(a b c)))

3

5.2 節(jié)展示了?let?表達(dá)式如何被理解成函數(shù)調(diào)用。?do?表達(dá)式同樣可以被解釋成調(diào)用遞歸函數(shù)忙上。這樣形式的?do?:

(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))

這個(gè)模型可以用來解決拷呆,任何你對(duì)于?do?行為仍有疑惑的問題。

6.3 參數(shù)列表 (Parameter Lists)

2.1 節(jié)我們演示過疫粥,有了前序表達(dá)式茬斧,?+?可以接受任何數(shù)量的參數(shù)。從那時(shí)開始梗逮,我們看過許多接受不定數(shù)量參數(shù)的函數(shù)项秉。要寫出這樣的函數(shù),我們需要使用一個(gè)叫做剩余(?rest?)參數(shù)的東西慷彤。

如果我們?cè)诤瘮?shù)的形參列表里的最后一個(gè)變量前娄蔼,插入?&rest?符號(hào),那么當(dāng)這個(gè)函數(shù)被調(diào)用時(shí)底哗,這個(gè)變量會(huì)被設(shè)成一個(gè)帶有剩余參數(shù)的列表∷晁撸現(xiàn)在我們可以明白funcall?是如何根據(jù)?apply?寫成的。它或許可以定義成:

[21]> (defun our-funcall (fn &rest args)

? (apply fn args))

我們也看過操作符中跋选,有的參數(shù)可以被忽略涕癣,并可以缺省設(shè)成特定的值。這樣的參數(shù)稱為選擇性參數(shù)(optional parameters)野建。(相比之下属划,普通的參數(shù)有時(shí)稱為必要參數(shù)「required parameters」) 如果符號(hào)?&optional?出現(xiàn)在一個(gè)函數(shù)的形參列表時(shí),

[22]> (defun philosoph (thing &optional property)

? (list thing 'is property))

那么在?&optional?之后的參數(shù)都是選擇性的候生,缺省為?nil?:

[25]> (philosoph 'death)

(DEATH IS NIL)

我們可以明確指定缺省值同眯,通過將缺省值附在列表里給入。這版的?philosoph

[26]> (defun philosoph (thing &optional (property 'fun))

? (list thing 'is property))

有著更鼓舞人心的缺省值:

[27]> (philosoph 'death)

(DEATH IS FUN)

選擇性參數(shù)的缺省值可以不是常量唯鸭⌒胛希可以是任何的 Lisp 表達(dá)式。若這個(gè)表達(dá)式不是常量,它會(huì)在每次需要用到缺省值時(shí)被重新求值明肮。

一個(gè)關(guān)鍵字參數(shù)(keyword parameter)是一種更靈活的選擇性參數(shù)菱农。如果你把符號(hào)?&key?放在一個(gè)形參列表,那在?&key?之后的形參都是選擇性的柿估。此外循未,當(dāng)函數(shù)被調(diào)用時(shí),這些參數(shù)會(huì)被識(shí)別出來秫舌,參數(shù)的位置在哪不重要的妖,而是用符號(hào)標(biāo)簽(譯注:?:?)識(shí)別出來:

[28]> (defun keylist (a &key x y z)

? ? (list a x y z))

KEYLIST

[29]> (keylist 1 :y 2)

(1 NIL 2 NIL)

[30]>? (keylist 1 :y 3 :x 2)

(1 2 3 NIL)

和普通的選擇性參數(shù)一樣,關(guān)鍵字參數(shù)缺省值為?nil?足陨,但可以在形參列表中明確地指定缺省值嫂粟。

關(guān)鍵字與其相關(guān)的參數(shù)可以被剩余參數(shù)收集起來,并傳遞給其他期望收到這些參數(shù)的函數(shù)墨缘。舉例來說星虹,我們可以這樣定義?adjoin?:

(defun our-adjoin (obj lst &rest args)

? (if (apply #'member obj lst args)

? ? ? lst

? ? ? (cons obj lst)))

由于?adjoin?與?member?接受一樣的關(guān)鍵字,我們可以用剩余參數(shù)收集它們镊讼,再傳給?member?函數(shù)宽涌。

5.2 節(jié)介紹過?destructuring-bind?宏。在通常情況下狠毯,每個(gè)模式(pattern)中作為第一個(gè)參數(shù)的子樹护糖,可以與函數(shù)的參數(shù)列表一樣復(fù)雜:

[32]> (destructuring-bind ((&key w x) &rest y) '((:w 3) a)

? (list w x y))

(3 NIL (A))

6.4 示例:實(shí)用函數(shù) (Example: Utilities)

2.6 節(jié)提到過,Lisp 大部分是由 Lisp 函數(shù)組成嚼松,這些函數(shù)與你可以自己定義的函數(shù)一樣嫡良。這是程序語言中一個(gè)有用的特色:你不需要改變你的想法來配合語言,因?yàn)槟憧梢愿淖冋Z言來配合你的想法献酗。如果你想要 Common Lisp 有某個(gè)特定的函數(shù)寝受,自己寫一個(gè),而這個(gè)函數(shù)會(huì)成為語言的一部分罕偎,就跟內(nèi)置的?+?或?eql?一樣很澄。

有經(jīng)驗(yàn)的 Lisp 程序員秦驯,由上而下(top-down)也由下而上 (bottom-up)地工作逛拱。當(dāng)他們朝著語言撰寫程序的同時(shí)佳鳖,也打造了一個(gè)更適合他們程序的語言帕膜。通過這種方式,語言與程序結(jié)合的更好研乒,也更好用掸鹅。

寫來擴(kuò)展 Lisp 的操作符稱為實(shí)用函數(shù)(utilities)把篓。當(dāng)你寫了更多 Lisp 程序時(shí)肄扎,會(huì)發(fā)現(xiàn)你開發(fā)了一系列的程序墨林,而在一個(gè)項(xiàng)目寫過許多的實(shí)用函數(shù)赁酝,下個(gè)項(xiàng)目里也會(huì)派上用場(chǎng)。

專業(yè)的程序員常發(fā)現(xiàn)旭等,手邊正在寫的程序酌呆,與過去所寫的程序有很大的關(guān)聯(lián)。這就是軟件重用讓人聽起來很吸引人的原因搔耕。但重用已經(jīng)被聯(lián)想成面向?qū)ο蟪绦蛟O(shè)計(jì)隙袁。但軟件不需要是面向?qū)ο蟮牟拍苤赜?── 這是很明顯的,我們看看程序語言(換言之度迂,編譯器)藤乙,是重用性最高的軟件。

要獲得可重用軟件的方法是惭墓,由下而上地寫程序,而程序不需要是面向?qū)ο蟮牟拍軌蛴上露系貙懗龆恪?shí)際上腊凶,函數(shù)式風(fēng)格相比之下,更適合寫出重用軟件拴念。想想看?sort?钧萍。在 Common Lisp 你幾乎不需要自己寫排序程序;?sort?是如此的快與普遍政鼠,以致于它不值得我們煩惱风瘦。這才是可重用軟件。

圖 6.1 實(shí)用函數(shù)

你可以通過撰寫實(shí)用函數(shù)公般,在程序里做到同樣的事情万搔。圖 6.1 挑選了一組實(shí)用的函數(shù)。前兩個(gè)?single??與?append1?函數(shù)官帘,放在這的原因是要演示瞬雹,即便是小程序也很有用。前一個(gè)函數(shù)?single??刽虹,當(dāng)實(shí)參是只有一個(gè)元素的列表時(shí)酗捌,返回真。

[38]>(single? '(a))

T

而后一個(gè)函數(shù)?append1?和?cons?很像涌哲,但在列表后面新增一個(gè)元素胖缤,而不是在前面:

[39]> (append1 '(a b c) 'd)

(A B C D)

下個(gè)實(shí)用函數(shù)是?map-int?,接受一個(gè)函數(shù)與整數(shù)?n?阀圾,并返回將函數(shù)應(yīng)用至整數(shù)?0?到?n-1?的結(jié)果的列表哪廓。

這在測(cè)試的時(shí)候非常好用(一個(gè) Lisp 的優(yōu)點(diǎn)之一是,互動(dòng)環(huán)境讓你可以輕松地寫出測(cè)試)稍刀。如果我們只想要一個(gè)?0?到?9?的列表撩独,我們可以:

[40]> (map-int #'identity 10)

(0 1 2 3 4 5 6 7 8 9)

然而要是我們想要一個(gè)具有 10 個(gè)隨機(jī)數(shù)的列表敞曹,每個(gè)數(shù)介于 0 至 99 之間(包含 99),我們可以忽略參數(shù)并只要:

[41]> (map-int #'(lambda (x) (random 100))

? ? ? ? ? 10)

(84 93 50 46 36 67 42 91 97 28)

map-int?的定義說明了 Lisp 構(gòu)造列表的標(biāo)準(zhǔn)做法(idiom)之一综膀。我們創(chuàng)建一個(gè)累積器?acc?澳迫,初始化是?nil?,并將之后的對(duì)象累積起來剧劝。當(dāng)累積完畢時(shí)橄登,反轉(zhuǎn)累積器。?[1]

我們?cè)?filter?中看到同樣的做法讥此。?filter?接受一個(gè)函數(shù)與一個(gè)列表拢锹,將函數(shù)應(yīng)用至列表元素上時(shí),返回所有非?nil?元素:

[42]> (filter #'(lambda (x)

? ? ? ? ? ? ? (and (evenp x) (+ x 10)))

? ? ? ? ? '(1 2 3 4 5 6 7))

(12 14 16)

另一種思考?filter?的方式是用通用版本的?remove-if?萄喳。

圖 6.1 的最后一個(gè)函數(shù)卒稳,?most?,根據(jù)某個(gè)評(píng)分函數(shù)(scoring function)他巨,返回列表中最高分的元素充坑。它返回兩個(gè)值,獲勝的元素以及它的分?jǐn)?shù):

[43]> (most #'length '((a b) (a b c) (a)))

(A B C) ;

3

如果平手的話染突,返回先馳得點(diǎn)的元素捻爷。

注意圖 6.1 的最后三個(gè)函數(shù),它們?nèi)邮芎瘮?shù)作為參數(shù)份企。 Lisp 使得將函數(shù)作為參數(shù)傳遞變得便捷也榄,而這也是為什么,Lisp 適合由下而上程序設(shè)計(jì)的原因之一司志。成功的實(shí)用函數(shù)必須是通用的甜紫,當(dāng)你可以將細(xì)節(jié)作為函數(shù)參數(shù)傳遞時(shí),要將通用的部份抽象起來就變得容易許多俐芯。

本節(jié)給出的函數(shù)是通用的實(shí)用函數(shù)棵介。可以用在任何種類的程序吧史。但也可以替特定種類的程序撰寫實(shí)用函數(shù)邮辽。確實(shí),當(dāng)我們談到宏時(shí)贸营,你可以凌駕于 Lisp 之上吨述,寫出自己的特定語言,如果你想這么做的話钞脂。如果你想要寫可重用軟件揣云,看起來這是最靠譜的方式。

6.5 閉包 (Closures)

函數(shù)可以如表達(dá)式的值冰啃,或是其它對(duì)象那樣被返回邓夕。以下是接受一個(gè)實(shí)參刘莹,并依其類型返回特定的結(jié)合函數(shù):

(defun combiner (x)

? (typecase x

? ? (number #'+)

? ? (list #'append)

? ? (t #'list)))

在這之上,我們可以創(chuàng)建一個(gè)通用的結(jié)合函數(shù):

(defun combine (&rest args)

? (apply (combiner (car args))

? ? ? ? args))

它接受任何類型的參數(shù)焚刚,并以適合它們類型的方式結(jié)合点弯。(為了簡(jiǎn)化這個(gè)例子,我們假定所有的實(shí)參矿咕,都有著一樣的類型抢肛。)

[46]> (combine 2 3)

5

[47]> (combine '(a b) '(c d))

(A B C D)

2.10 小節(jié)提過詞法變量(lexical variables)只在被定義的上下文內(nèi)有效。伴隨這個(gè)限制而來的是碳柱,只要那個(gè)上下文還有在使用捡絮,它們就保證會(huì)是有效的。

如果函數(shù)在詞法變量的作用域里被定義時(shí)莲镣,函數(shù)仍可引用到那個(gè)變量福稳,即便函數(shù)被作為一個(gè)值返回了,返回至詞法變量被創(chuàng)建的上下文之外剥悟。下面我們創(chuàng)建了一個(gè)把實(shí)參加上?3?的函數(shù):

[48]> (setf fn (let ((i 3))

? ? ? ? ? ? #'(lambda (x) (+ x i))))

#<FUNCTION :LAMBDA (X) (+ X I)>

[49]> (funcall fn 2)

5

當(dāng)函數(shù)引用到外部定義的變量時(shí)灵寺,這外部定義的變量稱為自由變量(free variable)。函數(shù)引用到自由的詞法變量時(shí)区岗,稱之為閉包(closure)。?[2]?只要函數(shù)還存在毁枯,變量就必須一起存在慈缔。

閉包結(jié)合了函數(shù)與環(huán)境(environment);無論何時(shí)种玛,當(dāng)一個(gè)函數(shù)引用到周圍詞法環(huán)境的某個(gè)東西時(shí)藐鹤,閉包就被隱式地創(chuàng)建出來了。這悄悄地發(fā)生在像是下面這個(gè)函數(shù)赂韵,是一樣的概念:

(defun add-to-list (num lst)

? (mapcar #'(lambda (x)

? ? ? ? ? ? ? (+ x num))

? ? ? ? ? lst))

這函數(shù)接受一個(gè)數(shù)字及列表娱节,并返回一個(gè)列表,列表元素是元素與傳入數(shù)字的和祭示。在 lambda 表達(dá)式里的變量?num?是自由的肄满,所以像是這樣的情況,我們傳遞了一個(gè)閉包給?mapcar?质涛。

一個(gè)更顯著的例子會(huì)是函數(shù)在被調(diào)用時(shí)稠歉,每次都返回不同的閉包。下面這個(gè)函數(shù)返回一個(gè)加法器(adder):

(defun make-adder (n)

? #'(lambda (x)

? ? ? (+ x n)))

它接受一個(gè)數(shù)字汇陆,并返回一個(gè)將該數(shù)字與其參數(shù)相加的閉包(函數(shù))怒炸。

[59]> (setf add3 (make-adder 3))

#<FUNCTION :LAMBDA (X) (+ X N)>

[60]> (funcall add3 2)

5

[61]> (setf add27 (make-adder 27))

#<FUNCTION :LAMBDA (X) (+ X N)>

[62]> (funcall add27 2)

29

我們可以產(chǎn)生共享變量的數(shù)個(gè)閉包。下面我們定義共享一個(gè)計(jì)數(shù)器的兩個(gè)函數(shù):

[65]> (let ((counter 0))

? (defun reset ()

? ? (setf counter 0))

? (defun stamp ()

? ? (setf counter (+ counter 1))))

STAMP

這樣的一對(duì)函數(shù)或許可以用來創(chuàng)建時(shí)間戳章(time-stamps)毡代。每次我們調(diào)用?stamp?時(shí)阅羹,我們獲得一個(gè)比之前高的數(shù)字勺疼,而調(diào)用?reset?我們可以將計(jì)數(shù)器歸零:

[66]> (list (stamp) (stamp) (reset) (stamp))

(1 2 0 1)

你可以使用全局計(jì)數(shù)器來做到同樣的事情,但這樣子使用計(jì)數(shù)器捏鱼,可以保護(hù)計(jì)數(shù)器被非預(yù)期的引用执庐。

Common Lisp 有一個(gè)內(nèi)置的函數(shù)?complement?函數(shù),接受一個(gè)謂詞穷躁,并返回謂詞的補(bǔ)數(shù)(complement)耕肩。比如:

[67]> (mapcar (complement #'oddp)

? ? ? ? ? '(1 2 3 4 5 6))

(NIL T NIL T NIL T)

有了閉包以后,很容易就可以寫出這樣的函數(shù):

(defun our-complement (f)

? #'(lambda (&rest args)

? ? ? (not (apply f args))))

如果你停下來好好想想问潭,會(huì)發(fā)現(xiàn)這是個(gè)非凡的小例子猿诸;而這僅是冰山一角。閉包是 Lisp 特有的美妙事物之一狡忙。閉包開創(chuàng)了一種在別的語言當(dāng)中梳虽,像是不可思議的程序設(shè)計(jì)方法。

6.6 示例:函數(shù)構(gòu)造器 (Example: Function Builders)

Dylan 是 Common Lisp 與 Scheme 的混合物灾茁,有著 Pascal 一般的語法窜觉。它有著大量返回函數(shù)的函數(shù):除了上一節(jié)我們所看過的?complement?,Dylan 包含:compose?北专、?disjoin?禀挫、?conjoin?、?curry?拓颓、?rcurry?以及?always?语婴。圖 6.2 有這些函數(shù)的 Common Lisp 實(shí)現(xiàn),而圖 6.3 演示了一些從定義延伸出的等價(jià)函數(shù)驶睦。

圖 6.2 Dylan 函數(shù)建構(gòu)器

首先砰左,?compose?接受一個(gè)或多個(gè)函數(shù),并返回一個(gè)依序?qū)⑵鋮?shù)應(yīng)用的新函數(shù)场航,即缠导,

(compose #'a #'b #'c)

返回一個(gè)函數(shù)等同于

#'(lambda (&rest args) (a (b (apply #'c args))))

這代表著?compose?的最后一個(gè)實(shí)參,可以是任意長(zhǎng)度溉痢,但其它函數(shù)只能接受一個(gè)實(shí)參僻造。

下面我們建構(gòu)了一個(gè)函數(shù),先給取參數(shù)的平方根适室,取整后再放回列表里嫡意,接著返回:

[80]> (mapcar (compose #'list #'round #'sqrt)

? ? ? ? ? '(4 9 16 25))

((2) (3) (4) (5))

下來的兩個(gè)函數(shù),?disjoin?及?conjoin?同接受一個(gè)或多個(gè)謂詞作為參數(shù):?disjoin?當(dāng)任一謂詞返回真時(shí)捣辆,返回真蔬螟,而?conjoin?當(dāng)所有謂詞返回真時(shí),返回真汽畴。

[81]> (mapcar (disjoin #'integerp #'symbolp)

? ? ? ? ? '(a "a" 2 3))

(T NIL T T)

[82]> (mapcar (conjoin #'integerp #'symbolp)

? ? ? ? ? '(a "a" 2 3))

(NIL NIL NIL NIL)

若考慮將謂詞定義成集合旧巾,?disjoin?返回傳入?yún)?shù)的聯(lián)集(union)耸序,而?conjoin?則是返回傳入?yún)?shù)的交集(intersection)。

圖 6.3 某些等價(jià)函數(shù)

函數(shù)?curry?與?rcurry?(“right curry”)精神上與前一小節(jié)的?make-adder?相同鲁猩。兩者皆接受一個(gè)函數(shù)及某些參數(shù)坎怪,并返回一個(gè)期望剩余參數(shù)的新函數(shù)。下列任一個(gè)函數(shù)等同于?(make-adder?3)?:

(curry #'+ 3)

(rcurry #'+ 3)

當(dāng)函數(shù)的參數(shù)順序重要時(shí)廓握,很明顯可以看出?curry?與?rcurry?的差別搅窿。如果我們?curry?#'-?,我們得到一個(gè)用其參數(shù)減去某特定數(shù)的函數(shù)隙券,

[87]> (funcall (curry #'- 3) 2)

1

而當(dāng)我們?rcurry?#'-?時(shí)男应,我們得到一個(gè)用某特定數(shù)減去其參數(shù)的函數(shù):

[88]> (funcall (rcurry #'- 3) 2)

-1

最后,?always?函數(shù)是 Common Lisp 函數(shù)?constantly?娱仔。接受一個(gè)參數(shù)并原封不動(dòng)返回此參數(shù)的函數(shù)沐飘。和?identity?一樣,在很多需要傳入函數(shù)參數(shù)的情況下很有用牲迫。

6.7 動(dòng)態(tài)作用域 (Dynamic Sc??ope)

2.11 小節(jié)解釋過局部與全局變量的差別耐朴。實(shí)際的差別是詞法作用域(lexical scope)的詞法變量(lexical variable),與動(dòng)態(tài)作用域(dynamic scope)的特別變量(special variable)的區(qū)別盹憎。但這倆幾乎是沒有區(qū)別筛峭,因?yàn)榫植孔兞繋缀蹩偸鞘窃~法變量,而全局變量總是是特別變量陪每。

在詞法作用域下蜒滩,一個(gè)符號(hào)引用到上下文中符號(hào)名字出現(xiàn)的地方。局部變量缺省有著詞法作用域奶稠。所以如果我們?cè)谝粋€(gè)環(huán)境里定義一個(gè)函數(shù),其中有一個(gè)變量叫做x?捡遍,

(let ((x 10))

? (defun foo ()

? ? x))

則無論?foo?被調(diào)用時(shí)有存在其它的?x?锌订,主體內(nèi)的?x?都會(huì)引用到那個(gè)變量:

[90]> (let ((x 20)) (foo))

10

而動(dòng)態(tài)作用域,我們?cè)诃h(huán)境中函數(shù)被調(diào)用的地方尋找變量画株。要使一個(gè)變量是動(dòng)態(tài)作用域的辆飘,我們需要在任何它出現(xiàn)的上下文中聲明它是?special?。如果我們這樣定義?foo?:

(let ((x 10))

? (defun foo ()

? ? (declare (special x))

? ? x))

則函數(shù)內(nèi)的?x?就不再引用到函數(shù)定義里的那個(gè)詞法變量谓传,但會(huì)引用到函數(shù)被調(diào)用時(shí)蜈项,當(dāng)下所存在的任何特別變量?x?:

[93]> (let ((x 20))

? ? (declare (special x))

? ? (foo))

20

新的變量被創(chuàng)建出來之后, 一個(gè)?declare?調(diào)用可以在代碼的任何地方出現(xiàn)续挟。?special?聲明是獨(dú)一無二的紧卒,因?yàn)樗梢愿淖兂绦虻男袨椤?13 章將討論其它種類的聲明。所有其它的聲明诗祸,只是給編譯器的建議跑芳;或許可以使程序運(yùn)行的更快轴总,但不會(huì)改變程序的行為。

通過在頂層調(diào)用?setf?來配置全局變量博个,是隱式地將變量聲明為特殊變量:

[94]> (setf x 30)

30

[95]> (setf x 30)

30

在一個(gè)文件里的代碼怀樟,如果你不想依賴隱式的特殊聲明,可以使用?defparameter?取代盆佣,讓程序看起來更簡(jiǎn)潔往堡。

動(dòng)態(tài)作用域什么時(shí)候會(huì)派上用場(chǎng)呢?通常用來暫時(shí)給某個(gè)全局變量賦新值共耍。舉例來說虑灰,有 11 個(gè)變量來控制對(duì)象印出的方式,包括了?*print-base*?征堪,缺省是?10?瘩缆。如果你想要用 16 進(jìn)制顯示數(shù)字,你可以重新綁定?*print-base*?:

[99]> (let ((*print-base* 16))

? ? (princ 32))

20

32

這里顯示了兩件事情佃蚜,由?princ?產(chǎn)生的輸出庸娱,以及它所返回的值。他們代表著同樣的數(shù)字谐算,第一次在被印出時(shí)熟尉,用 16 進(jìn)制顯示,而第二次洲脂,因?yàn)樵?let?表達(dá)式外部斤儿,所以是用十進(jìn)制顯示,因?yàn)?*print-base*?回到之前的數(shù)值恐锦,?10?往果。

6.8 編譯 (Compilation)

Common Lisp 函數(shù)可以獨(dú)立被編譯或挨個(gè)文件編譯。如果你只是在頂層輸入一個(gè)?defun?表達(dá)式:

[100]> (defun foo (x) (+ x 1))

FOO

許多實(shí)現(xiàn)會(huì)創(chuàng)建一個(gè)直譯的函數(shù)(interpreted function)一铅。你可以將函數(shù)傳給?compiled-function-p?來檢查一個(gè)函數(shù)是否有被編譯:

[101]> (compiled-function-p #'foo)

NIL

若你將?foo?函數(shù)名傳給?compile?:

[102]> (compile 'foo)

FOO?

則這個(gè)函數(shù)會(huì)被編譯陕贮,而直譯的定義會(huì)被編譯出來的取代。編譯與直譯函數(shù)的行為一樣潘飘,只不過對(duì)?compiled-function-p?來說不一樣肮之。

你可以把列表作為參數(shù)傳給?compile?。這種?compile?的用法在 161 頁 (譯注: 10.1 小節(jié))卜录。

有一種函數(shù)你不能作為參數(shù)傳給?compile?:一個(gè)像是?stamp?或是?reset?這種戈擒,在頂層明確使用詞法上下文輸入的函數(shù) (即?let?)?[3]?在一個(gè)文件里面定義這些函數(shù),接著編譯然后載入文件是可以的艰毒。這么限制直譯的代碼的是實(shí)作的原因筐高,而不是因?yàn)樵谠~法上下文里明確定義函數(shù)有什么問題。

通常要編譯 Lisp 代碼不是挨個(gè)函數(shù)編譯,而是使用?compile-file?編譯整個(gè)文件凯傲。這個(gè)函數(shù)接受一個(gè)文件名犬辰,并創(chuàng)建一個(gè)原始碼的編譯版本 ── 通常會(huì)有同樣的名稱,但不同的擴(kuò)展名冰单。當(dāng)編譯過的文件被載入時(shí)幌缝,?compiled-function-p?應(yīng)給所有定義在文件內(nèi)的函數(shù)返回真。

當(dāng)一個(gè)函數(shù)包含在另一個(gè)函數(shù)內(nèi)時(shí)诫欠,包含它的函數(shù)會(huì)被編譯涵卵,而且內(nèi)部的函數(shù)也會(huì)被編譯。所以?make-adder?(108 頁)被編譯時(shí)荒叼,它會(huì)返回編譯的函數(shù):

[104]> (compile 'make-adder)

MAKE-ADDER

[105]> (compiled-function-p (make-adder 2))

T

6.9 使用遞歸 (Using Recursion)

比起多數(shù)別的語言轿偎,遞歸在 Lisp 中扮演了一個(gè)重要的角色。這主要有三個(gè)原因:

1)函數(shù)式程序設(shè)計(jì)被廓。遞歸演算法有副作用的可能性較低坏晦。

2)遞歸數(shù)據(jù)結(jié)構(gòu)。 Lisp 隱式地使用了指標(biāo)嫁乘,使得遞歸地定義數(shù)據(jù)結(jié)構(gòu)變簡(jiǎn)單了昆婿。最常見的是用在列表:一個(gè)列表的遞歸定義,列表為空表蜓斧,或是一個(gè)?cons?仓蛆,其中?cdr?也是個(gè)列表。

3)優(yōu)雅性挎春。Lisp 程序員非常關(guān)心它們的程序是否美麗看疙,而遞歸演算法通常比迭代演算法來得優(yōu)雅。

學(xué)生們起初會(huì)覺得遞歸很難理解直奋。但 3.9 節(jié)指出了能庆,如果你想要知道是否正確,不需要去想遞歸函數(shù)所有的調(diào)用過程脚线。

同樣的如果你想寫一個(gè)遞歸函數(shù)相味。如果你可以描述問題是怎么遞歸解決的,通常很容易將解法轉(zhuǎn)成代碼殉挽。要使用遞歸來解決一個(gè)問題,你需要做兩件事:

1)你必須要示范如何解決問題的一般情況拓巧,通過將問題切分成有限小并更小的子問題斯碌。

2)你必須要示范如何通過 ── 有限的步驟,來解決最小的問題 ── 基本用例肛度。

如果這兩件事完成了傻唾,那問題就解決了。因?yàn)檫f歸每次都將問題變得更小,而一個(gè)有限的問題終究會(huì)被解決的冠骄,而最小的問題僅需幾個(gè)有限的步驟就能解決伪煤。

舉例來說,下面這個(gè)找到一個(gè)正規(guī)列表(proper list)長(zhǎng)度的遞歸算法凛辣,我們每次遞歸時(shí)抱既,都可以找到更小列表的長(zhǎng)度:

1)在一般情況下,一個(gè)正規(guī)列表的長(zhǎng)度是它的?cdr?加一扁誓。

2)基本用例防泵,空列表長(zhǎng)度為?0?。

當(dāng)這個(gè)描述翻譯成代碼時(shí)蝗敢,先處理基本用例捷泞;但公式化遞歸演算法時(shí),我們通常從一般情況下手寿谴。

前述的演算法锁右,明確地描述了一種找到正規(guī)列表長(zhǎng)度的方法。當(dāng)你定義一個(gè)遞歸函數(shù)時(shí)讶泰,你必須要確定你在分解問題時(shí)咏瑟,問題實(shí)際上越變?cè)叫 H〉靡粋€(gè)正規(guī)列表的?cdr?會(huì)給出?length?更小的子問題峻厚,但取得環(huán)狀列表(circular list)的?cdr?不會(huì)响蕴。

這里有兩個(gè)遞歸算法的示例。假定參數(shù)是有限的惠桃。注意第二個(gè)示例浦夷,我們每次遞歸時(shí),將問題分成兩個(gè)更小的問題:

第一個(gè)例子辜王,?member?函數(shù)劈狐,我們說某物是列表的成員,需滿足:如果它是第一個(gè)元素的成員或是?member?的?cdr?的成員呐馆。但空列表沒有任何成員肥缔。

第二個(gè)例子,?copy-tree?一個(gè)?cons?的?copy-tree?汹来,是一個(gè)由?cons?的?car?的?copy-tree?與?cdr?的?copy-tree?所組成的续膳。一個(gè)原子的?copy-tree?是它自己。

一旦你可以這樣描述算法收班,要寫出遞歸函數(shù)只差一步之遙坟岔。

某些算法通常是這樣表達(dá)最自然,而某些算法不是摔桦。你可能需要翻回前面社付,試試不使用遞歸來定義?our-copy-tree?(41 頁,譯注: 3.8 小節(jié))。另一方面來說鸥咖,23 頁 (譯注: 2.13 節(jié)) 迭代版本的?show-squares?可能更容易比 24 頁的遞歸版本要容易理解燕鸽。某些時(shí)候是很難看出哪個(gè)形式比較自然,直到你試著去寫出程序來啼辣。

如果你關(guān)心效率啊研,有兩個(gè)你需要考慮的議題。第一熙兔,尾遞歸(tail-recursive)悲伶,會(huì)在 13.2 節(jié)討論。一個(gè)好的編譯器住涉,使用循環(huán)或是尾遞歸的速度麸锉,應(yīng)該是沒有或是區(qū)別很小的。然而如果你需要使函數(shù)變成尾遞歸的形式時(shí)舆声,或許直接用迭代會(huì)更好花沉。

另一個(gè)需要銘記在心的議題是,最顯而易見的遞歸算法媳握,不一定是最有效的碱屁。經(jīng)典的例子是費(fèi)氏函數(shù)。它是這樣遞歸地被定義的蛾找,

Fib(0) = Fib(1) = 1

Fib(n) = Fib(n-1)+Fib(n-2)

直接翻譯這個(gè)定義娩脾,

(defun fib (n)

? (if (<= n 1)

? ? ? 1

? ? ? (+ (fib (- n 1))

? ? ? ? (fib (- n 2)))))

這樣是效率極差的。一次又一次的重復(fù)計(jì)算打毛。如果你要找?(fib?10)?柿赊,這個(gè)函數(shù)計(jì)算?(fib?9)?與?(fib?8)?。但要計(jì)算出?(fib?9)?幻枉,它需要再次計(jì)算?(fib?8)碰声,等等。

下面是一個(gè)算出同樣結(jié)果的迭代版本:

(defun fib (n)

? (do ((i n (- i 1))

? ? ? (f1 1 (+ f1 f2))

? ? ? (f2 1 f1))

? ? ? ((<= i 1) f1)))

迭代的版本不如遞歸版本來得直觀熬甫,但是效率遠(yuǎn)遠(yuǎn)高出許多胰挑。這樣的事情在實(shí)踐中常發(fā)生嗎?非常少 ── 這也是為什么所有的教科書都使用一樣的例子 ── 但這是需要注意的事

Chapter 6 總結(jié) (Summary)

1)命名函數(shù)是一個(gè)存在符號(hào)的?symbol-function?部分的函數(shù)椿肩。?defun?宏隱藏了這樣的細(xì)節(jié)瞻颂。它也允許你定義文檔字符串(documentation string),并指定setf?要怎么處理函數(shù)調(diào)用郑象。

2)定義局部函數(shù)是有可能的蘸朋,與定義局部變量有相似的精神。

3)函數(shù)可以有選擇性參數(shù)(optional)扣唱、剩余(rest)以及關(guān)鍵字(keyword)參數(shù)。

4)實(shí)用函數(shù)是 Lisp 的擴(kuò)展。他們是由下而上編程的小規(guī)模示例噪沙。

5)只要有某物引用到詞法變量時(shí)炼彪,它們會(huì)一直存在。閉包是引用到自由變量的函數(shù)正歼。你可以寫出返回閉包的函數(shù)辐马。

6)Dylan 提供了構(gòu)造函數(shù)的函數(shù)。很簡(jiǎn)單就可以使用閉包局义,然后在 Common Lisp 中實(shí)現(xiàn)它們喜爷。

7)特別變量(special variable)有動(dòng)態(tài)作用域 (dynamic scope)。

8)Lisp 函數(shù)可以單獨(dú)編譯萄唇,或(更常見)編譯整個(gè)文件檩帐。

9)一個(gè)遞歸演算法通過將問題細(xì)分成更小丶更小的子問題來解決問題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末另萤,一起剝皮案震驚了整個(gè)濱河市湃密,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌四敞,老刑警劉巖泛源,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異忿危,居然都是意外死亡达箍,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門铺厨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來缎玫,“玉大人,你說我怎么就攤上這事努释〉馍遥” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵伐蒂,是天一觀的道長(zhǎng)煞躬。 經(jīng)常有香客問我,道長(zhǎng)逸邦,這世上最難降的妖魔是什么恩沛? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮缕减,結(jié)果婚禮上雷客,老公的妹妹穿的比我還像新娘。我一直安慰自己桥狡,他們只是感情好搅裙,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布皱卓。 她就那樣靜靜地躺著,像睡著了一般部逮。 火紅的嫁衣襯著肌膚如雪娜汁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天兄朋,我揣著相機(jī)與錄音掐禁,去河邊找鬼。 笑死颅和,一個(gè)胖子當(dāng)著我的面吹牛傅事,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播峡扩,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼蹭越,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了有额?” 一聲冷哼從身側(cè)響起般又,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎巍佑,沒想到半個(gè)月后茴迁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萤衰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年堕义,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脆栋。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡倦卖,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出椿争,到底是詐尸還是另有隱情怕膛,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布秦踪,位于F島的核電站褐捻,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏椅邓。R本人自食惡果不足惜柠逞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望景馁。 院中可真熱鬧板壮,春花似錦、人聲如沸合住。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至笨使,卻和暖如春沪悲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背阱表。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留贡珊,地道東北人最爬。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像门岔,于是被迫代替她去往敵國(guó)和親爱致。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容