函數(shù)不僅是 Lisp 程序的根基,它同時(shí)也是 Lisp 語言的基石。
除了少數(shù)稱為特殊形式 (special form) 的操作符之外,Lisp 的核心就是一個(gè)函數(shù)的集合。如果你覺得某件事 Lisp 應(yīng)該能做,你完全可以把它寫出來,然后你的新函數(shù)可以享有和內(nèi)置函數(shù)同等的待遇。
有兩點(diǎn)讓 Lisp 函數(shù)與眾不同。一是Lisp 本身就是函數(shù)的集合。這意味著我們可以自己給 Lisp 增加新的操作符。另一個(gè)就是:函數(shù)也是 Lisp 的對(duì)象。這意味著在 Lisp 里我們可以像對(duì)待其他熟悉的數(shù)據(jù)類型那樣來對(duì)待函數(shù),就像整數(shù)那樣:在運(yùn)行期創(chuàng)建一個(gè)新函數(shù),把函數(shù)保存在變量和結(jié)構(gòu)體里面,把它作為參數(shù)傳給其他函數(shù),還有把它作為函數(shù)的返回值。
定義函數(shù)
最通常的做法就是用defun宏實(shí)現(xiàn)函數(shù)定義,如下面定義的double函數(shù)。
> (defun double (x) (* 2 x))
DOUBLE
函數(shù)本身就是對(duì)象,defun就是構(gòu)造這樣的對(duì)象,然后保存它到第一個(gè)參數(shù)名下。所以我們既可以調(diào)用double,也可以持有這個(gè)名字對(duì)應(yīng)的函數(shù)對(duì)象。通??梢杂?code>#'操作符得到這個(gè)對(duì)象,這個(gè)操作符能將名字映射到實(shí)際的函數(shù)對(duì)象。就像下面這樣:
> #'double
#<FUNCTION DOUBLE>
也可以使用On Lisp
可以使用一種稱為 λ–表達(dá)式 (lambda-expression) 的東西定義函數(shù),它由三部分組成:lambda符號(hào),參數(shù)列表和若干表達(dá)式主體。下面的λ–表達(dá)式是等價(jià)于double的函數(shù):
> (lambda (x) (* 2 x))
λ–表達(dá)式也可以看作是函數(shù)的名字,如果說 double 是個(gè)正規(guī)的名字,那么lambda表達(dá)式就相當(dāng)于具體的描述。通過#'可以得到相應(yīng)的函數(shù):
> #'(lambda (x) (* 2 x))
#<FUNCTION (LAMBDA (X)) {100366DEAB}>
由于 λ–表達(dá)式同時(shí)也是函數(shù)的名字,因而它也可以出現(xiàn)在函數(shù)調(diào)用的最前面:
> ((lambda (x) (* x 2)) 3)
6
在 Common Lisp 里,我們可以同時(shí)擁有名為 double 的函數(shù)和變量。
"擁有分開的變量和函數(shù)命名空間的 Lisp 稱為 Lisp–2,在另一類 Lisp–1 下,變量和函數(shù)定義在同一命名空間里,最著名的這種 Lisp 方言是 Scheme。關(guān)于 Lisp–1 vs. Lisp–2 在網(wǎng)上有很多討論,一般觀點(diǎn)認(rèn)為 Lisp–1 對(duì)于編譯器來說更難實(shí)現(xiàn)。"
> (setq double 2)
2
> (double double)
4
當(dāng)符號(hào)出現(xiàn)在函數(shù)調(diào)用的首位,或者前置 #’ 的時(shí)候,它被認(rèn)為是函數(shù)。其他場合下它被當(dāng)成變量名。
Common Lisp 還提供了兩個(gè)函數(shù)用于將符號(hào)映射到它所代表的函數(shù)或者變量,以備不時(shí)之需。
symbol-value 函數(shù)以一個(gè)符號(hào)為參數(shù),返回對(duì)應(yīng)變量的值:
> (symbol-value 'double)
2
而symbol-function則可以得到一個(gè)全局定義的函數(shù):
> (symbol-function 'double)
#<FUNCTION DOUBLE>
由于函數(shù)也是對(duì)象,變量也可以把函數(shù)當(dāng)作值
(setf double-1 #'double)
其實(shí),defun實(shí)際上是把它的第一個(gè)參數(shù)的symbol-function設(shè)置成了用它其余部分構(gòu)造的函數(shù),下面兩個(gè)表達(dá)式完成的功能基本相同:
(defun double (x) (* 2 x))
(setf (symbol-function 'double) #'(lambda (x) (* 2 x)))
函數(shù)型參數(shù)
函數(shù)同為數(shù)據(jù)對(duì)象,就意味著我們可以像對(duì)待其他對(duì)象那樣把它傳遞給其他函數(shù)。common lisp中提供了apply和funcall讓我們實(shí)現(xiàn)對(duì)函數(shù)的調(diào)用:
(+ 1 2)
(apply #’+ ’(1 2))
(apply (symbol-function ’+) ’(1 2))
(apply #’(lambda (x y) (+ x y)) ’(1 2))
(apply #’+ 1 ’(2))
(funcall #’+ 1 2)
apply和funcall的唯一區(qū)別就是,apply必須以列表參數(shù)結(jié)尾。
作為屬性的函數(shù)
函數(shù)作為 Lisp對(duì)象這一事實(shí)也創(chuàng)造了條件,讓我們能夠編寫出那種可以隨時(shí)擴(kuò)展以滿足新需求的程序。假設(shè)我們需要寫一個(gè)以動(dòng)物種類作為參數(shù)并產(chǎn)生相應(yīng)行為的函數(shù)。在大多數(shù)語言中,會(huì)使用 case 語句達(dá)到這個(gè)目的,Lisp 里也可以用同樣的辦法:
(defun behave (animal)
(case animal
(dog (wag-tail) (bark))
(rat (scurry) (squeak))
(cat (rub-legs) (scratch-carpet))))
如果把每種個(gè)體動(dòng)物的行為以單獨(dú)的函數(shù)形式保存,會(huì)更利于擴(kuò)展:
(defun behave (animal) (funcall (get animal ’behavior)))
(setf (get ’dog ’behavior)
#’(lambda () (wag-tail) (bark)))
作用域
Common Lisp 是一種詞法作用域 (lexically scope) 的 Lisp方言,詞法作用域與動(dòng)態(tài)作用域的區(qū)別在于語言處理自由變量的方式不同。
當(dāng)一個(gè)符號(hào)被用來表達(dá)變量時(shí),我們稱這個(gè)符號(hào)在表達(dá)式中是被綁定(bound)的,這里的變量可以是參數(shù),也可以來自于像let,do這樣的變量綁定操作符。如果符號(hào)不受到約束,就認(rèn)為它是自由的,比如:
(let ((y 7))
(defun scope-test (x)
(list x y)))
在函數(shù)表達(dá)式里,x是受約束的,而y是自由的。自由變量有意思的地方就在于,這種變量應(yīng)有的值并不那么顯而易見。一個(gè)約束變量的值是確信無疑的,當(dāng)調(diào)用 scope-test 時(shí),x 的值就是通過參數(shù)傳給它的值。但 y 的值應(yīng)該是什么呢?這要看具體方言的作用域規(guī)則。
在 動(dòng)態(tài)作用域 的 Lisp 里,要想找出當(dāng) scope-test 執(zhí)行時(shí)自由變量的值,我們要往回逐個(gè)檢查函數(shù)的調(diào)用鏈。當(dāng)發(fā)現(xiàn) y 被綁定時(shí),這個(gè)被綁定的值即被用在 scope-test 中。如果沒有發(fā)現(xiàn),那就取 y 的全局值。這樣,在用動(dòng)態(tài)作用域的 Lisp 里,在調(diào)用的時(shí)候 y 將會(huì)產(chǎn)生這樣的值:
> (let ((y 5)) (scope-test 3))
(3 5)
在 詞法作用域 的 Lisp 里,我們不再往回逐個(gè)檢查函數(shù)的調(diào)用鏈,而是逐層檢查定義這個(gè)函數(shù)時(shí),它所處的各層外部環(huán)境。在一個(gè)詞法作用域 Lisp 里,我們的示例將捕捉到定義 scope-test 時(shí),變量 y 的綁定。所以可以在 Common Lisp 里觀察到下面的現(xiàn)象:
> (let ((y 5)) (scope-test 3))
(3 7)
閉包
common lisp是詞法作用域,如果定義含有自由變量的函數(shù),系統(tǒng)就必須在函數(shù)定義時(shí)保存那些變量的綁定。這種函數(shù)和一組變量綁定的組合稱為 閉包。閉包是帶有局部狀態(tài)的函數(shù)。下面列舉一些這樣的例子:
(defun list+ (lst n)
(mapcar #’(lambda (x) (+ x n))
lst))
> (list+ ’(1 2 3) 10)
(11 12 13)
(defun make-adderb (n)
#’(lambda (x &optional change)
(if change
(setq n x)
(+ x n))))
> (setq addx (make-adderb 1))
> (funcall addx 3)
4
> (funcall addx 100 t)
100
> (funcall addx 3)
103
局部函數(shù)
在用lambda表達(dá)式時(shí),由于它沒有自己的名字,因此沒辦法引用自己,這就意味著在common lisp里,不能用lambda表達(dá)式定義遞歸函數(shù)。
如果我們想把某個(gè)列表上的參數(shù)一一應(yīng)用到某個(gè)函數(shù)上,可以這樣寫:
> (mapcar #'(lambda (x) (+ 2 x))
'(1 2 3 4))
(3 4 5 6)
要是想把遞歸函數(shù)作為第一個(gè)參數(shù)送給 mapcar 呢?如果函數(shù)已經(jīng)用 defun 定義了,我們就可以通過名
字簡單地引用它:
> (mapcar #’copy-tree ’((a b) (c d e)))
((A B) (C D E))
但現(xiàn)在假設(shè)這個(gè)函數(shù)必須是一個(gè)閉包,它從 mapcar 所處的環(huán)境獲得綁定,比如:
(defun list+ (lst n)
(mapcar #’(lambda (x) (+ x n))
lst))
mapcar 的第一個(gè)參數(shù)是 #’(lambda (x) (+ x n)),它必須要在 list+ 里定義,原因是它需要捕捉 n 的綁定。到目前為止都還一切正常,但如果要給 mapcar 傳遞一個(gè)函數(shù),而這個(gè)函數(shù)在需要局部綁定的同時(shí)也是遞歸的呢?我們不能使用一個(gè)在其他地方通過 defun 定義的函數(shù),因?yàn)檫@需要局部環(huán)境的綁定。并且我們也不能使用 lambda 來定義一個(gè)遞歸函數(shù),因?yàn)檫@個(gè)函數(shù)將無法引用其自身。
Common Lisp 提供了 labels 幫助我們跳出這個(gè)兩難的困境。除了在一個(gè)重要方面有所保留外,labels 基本可以看作是 let 的函數(shù)版本。labels 表達(dá)式里的每個(gè)綁定規(guī)范都必須符合如下形式:
(?name? ?parameters? . ?body?)
在 labels 表達(dá)式里,?name? 將指向與下面表達(dá)式等價(jià)的函數(shù):
#’(lambda ?parameters? . ?body?)
例如:
> (labels ((inc (x) (1+ x)))
(inc 3))
> 4
盡管如此,在 let 與 labels 之間有一個(gè)重要的區(qū)別。在 let 表達(dá)式里,變量的值不能依賴于同一
個(gè) let 里生成的另一個(gè)變量,就是說,你不能這樣寫( let*可以這樣寫 ):
(let ((x 10)
(y x))
y)
然后認(rèn)為這個(gè)新的 y 能反映出那個(gè)新 x 的值。相反,在 labels 里定義的函數(shù) f 的函數(shù)體里就可以引用那里定義的其他函數(shù),包括 f 本身,這就使定義遞歸函數(shù)成為可能。
使用 labels,我們就可以寫出類似 list+ 這樣的函數(shù)了,但這里 mapcar 的第一個(gè)參數(shù)是遞歸函數(shù):
(defun count-instances (obj lsts)
(labels ((instances-in (lst)
(if (consp lst)
(+ (if (eq (car lst) obj) 1 0)
(instances-in (cdr lst)))
0)))
(mapcar #’instances-in lsts)))
該函數(shù)接受一個(gè)對(duì)象和一個(gè)列表,然后分別統(tǒng)計(jì)出該對(duì)象在列表的每個(gè)元素 (作為列表) 中出現(xiàn)的次數(shù),把這些次數(shù)組成列表,并返回它:
> (count-instances ’a ’((a b c) (d a r p a) (d a r) (a a)))
(1 2 1 2)
尾遞歸
遞歸函數(shù)自己調(diào)用自己。如果函數(shù)調(diào)用自己之后不做其他工作,這種調(diào)用就稱為 尾遞歸 (tail-
recursive)。下面這個(gè)函數(shù)不是尾遞歸的:
(defun our-length (lst)
(if (null lst)
0
(1+ (out-length (cdr lst)))))
因?yàn)樵趶倪f歸調(diào)用返回之后,我們又把結(jié)果傳給了 1+。
尾遞歸是一種令人青睞的特性,因?yàn)樵S多 Common Lisp 編譯器都可以把尾遞歸轉(zhuǎn)化成循環(huán)。若使用這種編譯器,你就可以在源代碼里書寫優(yōu)雅的遞歸,而不必?fù)?dān)心函數(shù)調(diào)用在運(yùn)行期產(chǎn)生的系統(tǒng)開銷。如果一個(gè)函數(shù)不是尾遞歸的話,常??梢园岩粋€(gè)使用累積器 (accumulator) 的局部函數(shù)嵌入到其中,用這種方法把它轉(zhuǎn)換成尾遞歸的形式。在這里,累積器指的是一個(gè)參數(shù),它代表著到目前為止計(jì)算得到的值。例如 our-length 可以轉(zhuǎn)換成
(defun our-length (lst)
(labels ((rec (lst acc)
(if (null lst)
acc
(rec (cdr lst) (1+ acc)))))
(rec lst 0)))