因?yàn)镾cheme是函數(shù)式語言,通常來說,你可以編寫不使用賦值的語句。然而,如果使用賦值的話,有些算法就可以輕易實(shí)現(xiàn)了。尤其是內(nèi)部狀態(tài)和繼續(xù)(continuations )需要賦值。
盡管賦值非常習(xí)見并且易于理解,但它有一些本質(zhì)上的缺陷。參見《計(jì)算機(jī)程序的構(gòu)造和解釋》的第三章第一節(jié)“賦值和局部狀態(tài)”以及《為什么函數(shù)式編程如此重要》
R5RS中規(guī)定的用于賦值的特殊形式是set!
、set-car!
、set-cdr!
、string-set!
、vector-set!
等。除此之外,有些實(shí)現(xiàn)也依賴于賦值。由于賦值改變了參數(shù)的值,因此它具有破壞性(destructive)。Scheme中,具有破壞性的方法都以!
結(jié)尾,以警示程序員。
set!
可以為一個(gè)參數(shù)賦值。與Common Lisp不同,set!
無法給一個(gè)S-表達(dá)式賦值。 賦值前參數(shù)應(yīng)被定義。
(define var 1)
(set! var (* var 10))
var ? 10
(let ((i 1))
(set! i (+ i 3))
i)
? 4
Scheme中變量的作用域被限定在了源碼中定義其的那個(gè)括號(hào)里。作用域與源代碼書寫方式一致的作用域稱為“詞法閉包(Lexical closure)”或“靜態(tài)作用域(Static scope)”。This way of scope eliminates bags, as you can grasp the scope of parameters quite easily — written on the source code.另一方面,還有一種被稱為“動(dòng)態(tài)作用域(Dynamic scope)”的作用域。這種作用域僅在程序運(yùn)行時(shí)確定。由于會(huì)在調(diào)試時(shí)帶來種種問題,這種作用域現(xiàn)在已經(jīng)不再使用。
特殊形式let
、lambda
、letrec
生成閉包。lambda表達(dá)式的參數(shù)僅在函數(shù)定義內(nèi)部有效。let
只是lambda
的語法糖,因此二者無異。
你可以使用詞法閉包來實(shí)現(xiàn)帶有內(nèi)部狀態(tài)的過程。例如,用于模擬銀行賬戶的過程可以按如下的方式編寫:初始資金是10美元。函數(shù)接收一個(gè)整形參數(shù)。正數(shù)表示存入,負(fù)數(shù)表示取出。為了簡單起見,這里允許存款為負(fù)數(shù)。
(define bank-account
(let ((balance 10))
(lambda (n)
(set! balance (+ balance n))
balance)))
該過程將存款賦值為(+ balance n)
。下面是調(diào)用這個(gè)過程的結(jié)果。
The procedure assigns?(+ balance n)?to the?balance.??Following is the result of calling this function.
(bank-account 20) ; donating 20 dollars
;Value: 30
(bank-account -25) ; withdrawing 25 dollars
;Value: 5
因?yàn)樵赟cheme中,你可以編寫返回過程的過程,因此你可以編寫一個(gè)創(chuàng)建銀行賬戶的函數(shù)。這個(gè)例子喻示著使用函數(shù)式程序設(shè)計(jì)語言可以很容易實(shí)現(xiàn)面向?qū)ο蟪绦蛟O(shè)計(jì)語言。實(shí)際上,只需要在這個(gè)基礎(chǔ)上再加一點(diǎn)東西就可以實(shí)現(xiàn)一門面向?qū)ο蟪绦蛟O(shè)計(jì)語言了。
(define (make-bank-account balance)
(lambda (n)
(set! balance (+ balance n))
balance))
(define gates-bank-account (make-bank-account 10)) ; Gates makes a bank account by donating 10 dollars
;Value: gates-bank-account
(gates-bank-account 50) ; donating 50 dollars
;Value: 60
(gates-bank-account -55) ; withdrawing 55 dollars
;Value: 5
(define torvalds-bank-account (make-bank-account 100)) ; Torvalds makes a bank account by donating 100 dollars
;Value: torvalds-bank-account
(torvalds-bank-account -70) ; withdrawing 70 dollars
;Value: 30
(torvalds-bank-account 300) ; donating 300 dollars
;Value: 330
Scheme過程的主要目的是返回一個(gè)值,而另一個(gè)目的則稱為副作用(Side Effect)。賦值和IO操作就是副作用。
練習(xí) 1
修改make-bank-account函數(shù) Modify?make-bank-account?so that withdrawing more than balance causes error.?> hint: Use?begin?to group more than one S-expressions.
函數(shù)set-cat!和set-cdr!分別為一個(gè)cons單元的car部分和cdr部分賦新值。和set!不同,這兩個(gè)操作可以為S-表達(dá)式賦值。
(define tree '((1 2) (3 4 5) (6 7 8 9)))
(set-car! (car tree) 100) ; changing 1 to 100
tree
((100 2) (3 4 5) (6 7 8 9))
(set-cdr! (third tree) '(a b c)) ; changing '(7 8 9) to '(a b c)
tree
? ((100 2) (3 4 5) (6 a b c))
隊(duì)列可以用set-car!和set-cdr!實(shí)現(xiàn)。隊(duì)列是一種先進(jìn)先出(First in first out, FIFO)的數(shù)據(jù)結(jié)構(gòu),表則是先進(jìn)后出(First in last out,F(xiàn)ILO)。圖表1展示了隊(duì)列的結(jié)構(gòu)。cons-cell-top
的car部分指向表(頭),而(cons-cell-top
的)cdr部分指向表末的cons單元(表尾)。
入隊(duì)操作按如下步驟進(jìn)行(見圖表2): 1. 將當(dāng)前最末的cons單元(可以通過cons-cell-top
取得)的cdr部分重定向到新的元素。 2. 將cons-cell-top
的cdr部分重定向到新的元素
出隊(duì)操作按如下步驟進(jìn)行(見圖表3) 1. 將隊(duì)首元素存放在一個(gè)局部變量里。 2. 將cons-cell-top
的car部分重定向到表的第二個(gè)元素
[代碼片段1]展示了如何實(shí)現(xiàn)隊(duì)列。函數(shù)enqueue!返回將元素obj添加進(jìn)隊(duì)列queue后的隊(duì)列。函數(shù)dequeue!將隊(duì)列的首元素移出隊(duì)列并將該元素的值作為返回值。
[代碼片段1] 隊(duì)列的Scheme實(shí)現(xiàn)
(define (make-queue)
(cons '() '()))
(define (enqueue! queue obj)
(let ((lobj (cons obj '())))
(if (null? (car queue))
(begin
(set-car! queue lobj)
(set-cdr! queue lobj))
(begin
(set-cdr! (cdr queue) lobj)
(set-cdr! queue lobj)))
(car queue)))
(define (dequeue! queue)
(let ((obj (car (car queue))))
(set-car! queue (cdr (car queue)))
obj))
(define q (make-queue))
;Value: q
(enqueue! q 'a)
;Value 12: (a)
(enqueue! q 'b)
;Value 12: (a b)
(enqueue! q 'c)
;Value 12: (a b c)
(dequeue! q)
;Value: a
q
;Value 13: ((b c) c)
這一章中,我講解了賦值和變量的作用域。雖然在Scheme中,賦值并不常用,但它對(duì)于某些算法和數(shù)據(jù)結(jié)構(gòu)來說是必不可少的。濫用賦值會(huì)讓你的代碼丑陋。當(dāng)萬不得已時(shí)才使用賦值!在后面的幾章里,我會(huì)介紹Scheme中的數(shù)據(jù)結(jié)構(gòu)。
(define (make-bank-account amount)
(lambda (n)
(let ((m (+ amount n)))
(if (negative? m)
'error
(begin
(set! amount m)
amount)))))
更多建議: