2.2 節(jié)介紹過 Common Lisp 的求值規(guī)則,現(xiàn)在你應(yīng)該很熟悉了。本章的操作符都有一個共同點,就是它們都違反了求值規(guī)則。這些操作符讓你決定在程序當中何時要求值。如果普通的函數(shù)調(diào)用是 Lisp 程序的樹葉的話,那這些操作符就是連結(jié)樹葉的樹枝。
Common Lisp 有三個構(gòu)造區(qū)塊(block)的基本操作符:?progn
?、?block
?以及?tagbody
?。我們已經(jīng)看過?progn
?了。在?progn
?主體中的表達式會依序求值,并返回最后一個表達式的值:
> (progn
(format t "a")
(format t "b")
(+ 11 12))
ab
23
由于只返回最后一個表達式的值,代表著使用?progn
?(或任何區(qū)塊)涵蓋了副作用。
一個?block
?像是帶有名字及緊急出口的?progn
?。第一個實參應(yīng)為符號。這成為了區(qū)塊的名字。在主體中的任何地方,可以停止求值,并通過使用?return-from
?指定區(qū)塊的名字,來立即返回數(shù)值:
> (block head
(format t "Here we go.")
(return-from head 'idea)
(format t "We'll never see this."))
Here we go.
IDEA
調(diào)用?return-from
?允許你的程序,從代碼的任何地方,突然但優(yōu)雅地退出。第二個傳給?return-from
?的實參,用來作為以第一個實參為名的區(qū)塊的返回值。在?return-from
?之后的表達式不會被求值。
也有一個?return
?宏,它把傳入的參數(shù)當做封閉區(qū)塊?nil
?的返回值:
> (block nil
(return 27))
27
許多接受一個表達式主體的 Common Lisp 操作符,皆隱含在一個叫做?nil
?的區(qū)塊里。比如,所有由?do
?構(gòu)造的迭代函數(shù):
> (dolist (x '(a b c d e))
(format t "~A " x)
(if (eql x 'c)
(return 'done)))
A B C
DONE
使用?defun
?定義的函數(shù)主體,都隱含在一個與函數(shù)同名的區(qū)塊,所以你可以:
(defun foo ()
(return-from foo 27))
在一個顯式或隱式的?block
?外,不論是?return-from
?或?return
?都不會工作。
使用?return-from
?,我們可以寫出一個更好的?read-integer
?版本:
(defun read-integer (str)
(let ((accum 0))
(dotimes (pos (length str))
(let ((i (digit-char-p (char str pos))))
(if i
(setf accum (+ (* accum 10) i))
(return-from read-integer nil))))
accum))
68 頁的版本在構(gòu)造整數(shù)之前,需檢查所有的字符?,F(xiàn)在兩個步驟可以結(jié)合,因為如果遇到非數(shù)字的字符時,我們可以舍棄計算結(jié)果。出現(xiàn)在主體的原子(atom)被解讀為標簽(labels);把這樣的標簽傳給?go
?,會把控制權(quán)交給標簽后的表達式。以下是一個非常丑的程序片段,用來印出一至十的數(shù)字:
> (tagbody
(setf x 0)
top
(setf x (+ x 1))
(format t "~A " x)
(if (< x 10) (go top)))
1 2 3 4 5 6 7 8 9 10
NIL
這個操作符主要用來實現(xiàn)其它的操作符,不是一般會用到的操作符。大多數(shù)迭代操作符都隱含在一個?tagbody
?,所以是可能可以在主體里(雖然很少想要)使用標簽及?go
?。
如何決定要使用哪一種區(qū)塊建構(gòu)子呢(block construct)?幾乎任何時候,你會使用?progn
?。如果你想要突然退出的話,使用block
?來取代。多數(shù)程序員永遠不會顯式地使用?tagbody
?。
另一個我們用來區(qū)分表達式的操作符是?let
?。它接受一個代碼主體,但允許我們在主體內(nèi)設(shè)置新變量:
> (let ((x 7)
(y 2))
(format t "Number")
(+ x y))
Number
9
一個像是?let
?的操作符,創(chuàng)造出一個新的詞法語境(lexical context)。在這個語境里有兩個新變量,然而在外部語境的變量也因此變得不可視了。
概念上說,一個?let
?表達式等同于函數(shù)調(diào)用。在 2.14 節(jié)證明過,函數(shù)可以用名字來引用,也可以通過使用一個 lambda 表達式從字面上來引用。由于 lambda 表達式是函數(shù)的名字,我們可以像使用函數(shù)名那樣,把 lambda 表達式作為函數(shù)調(diào)用的第一個實參:
> ((lambda (x) (+ x 1)) 3)
4
前述的?let
?表達式,實際上等同于:
((lambda (x y)
(format t "Number")
(+ x y))
7
2)
如果有關(guān)于?let
?的任何問題,應(yīng)該是如何把責(zé)任交給?lambda
?,因為進入一個?let
?等同于執(zhí)行一個函數(shù)調(diào)用。
這個模型清楚的告訴我們,由?let
?創(chuàng)造的變量的值,不能依賴其它由同一個?let
?所創(chuàng)造的變量。舉例來說,如果我們試著:
(let ((x 2)
(y (+ x 1)))
(+ x y))
在?(+?x?1)
?中的?x
?不是前一行所設(shè)置的值,因為整個表達式等同于:
((lambda (x y) (+ x y)) 2
(+ x 1))
這里明顯看到?(+?x?1)
?作為實參傳給函數(shù),不能引用函數(shù)內(nèi)的形參?x
?。
所以如果你真的想要新變量的值,依賴同一個表達式所設(shè)立的另一個變量?在這個情況下,使用一個變形版本?let*
?:
> (let* ((x 1)
(y (+ x 1)))
(+ x y))
3
一個?let*
?功能上等同于一系列嵌套的?let
?。這個特別的例子等同于:
(let ((x 1))
(let ((y (+ x 1)))
(+ x y)))
let
?與?let*
?將變量初始值都設(shè)為?nil
?。nil
?為初始值的變量,不需要依附在列表內(nèi):
> (let (x y)
(list x y))
(NIL NIL)
destructuring-bind
?宏是通用化的?let
?。其接受單一變量,一個模式 (pattern) ── 一個或多個變量所構(gòu)成的樹 ── 并將它們與某個實際的樹所對應(yīng)的部份做綁定。舉例來說:
> (destructuring-bind (w (x y) . z) '(a (b c) d e)
(list w x y z))
(A B C (D E))
若給定的樹(第二個實參)沒有與模式匹配(第一個參數(shù))時,會產(chǎn)生錯誤。
最簡單的條件式是?if
?;其余的條件式都是基于?if
?所構(gòu)造的。第二簡單的條件式是?when
?,它接受一個測試表達式(test expression)與一個代碼主體。若測試表達式求值返回真時,則對主體求值。所以
(when (oddp that)
(format t "Hmm, that's odd.")
(+ that 1))
等同于
(if (oddp that)
(progn
(format t "Hmm, that's odd.")
(+ that 1)))
when
?的相反是?unless
?;它接受相同的實參,但僅在測試表達式返回假時,才對主體求值。
所有條件式的母體 (從正反兩面看) 是?cond
?,?cond
?有兩個新的優(yōu)點:允許多個條件判斷,與每個條件相關(guān)的代碼隱含在?progn
?里。cond
?預(yù)期在我們需要使用嵌套?if
?的情況下使用。 舉例來說,這個偽 member 函數(shù)
(defun our-member (obj lst)
(if (atom lst)
nil
(if (eql (car lst) obj)
lst
(our-member obj (cdr lst)))))
也可以定義成:
(defun our-member (obj lst)
(cond ((atom lst) nil)
((eql (car lst) obj) lst)
(t (our-member obj (cdr lst)))))
事實上,Common Lisp 實現(xiàn)大概會把?cond
?翻譯成?if
?的形式。
總得來說呢,?cond
?接受零個或多個實參。每一個實參必須是一個具有條件式,伴隨著零個或多個表達式的列表。當?cond
?表達式被求值時,測試條件式依序求值,直到某個測試條件式返回真才停止。當返回真時,與其相關(guān)聯(lián)的表達式會被依序求值,而最后一個返回的數(shù)值,會作為?cond
?的返回值。如果符合的條件式之后沒有表達式的話:
> (cond (99))
99
則會返回條件式的值。
由于?cond
?子句的?t
?條件永遠成立,通常我們把它放在最后,作為缺省的條件式。如果沒有子句符合時,則?cond
?返回?nil
?,但利用nil
?作為返回值是一種很差的風(fēng)格 (這種問題可能發(fā)生的例子,請看 292 頁)。譯注:?Appendix A, unexpected nil?小節(jié)。
當你想要把一個數(shù)值與一系列的常量比較時,有?case
?可以用。我們可以使用?case
?來定義一個函數(shù),返回每個月份中的天數(shù):
(defun month-length (mon)
(case mon
((jan mar may jul aug oct dec) 31)
((apr jun sept nov) 30)
(feb (if (leap-year) 29 28))
(otherwise "unknown month")))
一個?case
?表達式由一個實參開始,此實參會被拿來與每個子句的鍵值做比較。接著是零個或多個子句,每個子句由一個或一串鍵值開始,跟隨著零個或多個表達式。鍵值被視為常量;它們不會被求值。第一個參數(shù)的值被拿來與子句中的鍵值做比較 (使用?eql
?)。如果匹配時,子句剩余的表達式會被求值,并將最后一個求值作為?case
?的返回值。
缺省子句的鍵值可以是?t
?或?otherwise
?。如果沒有子句符合時,或是子句只包含鍵值時,
> (case 99 (99))
NIL
則?case
?返回?nil
?。
typecase
?宏與?case
?相似,除了每個子句中的鍵值應(yīng)為類型修飾符 (type specifiers),以及第一個實參與鍵值比較的函數(shù)使用typep
?而不是?eql
?(一個?typecase
?的例子在 107 頁)。?譯注: 6.5 小節(jié)。
最基本的迭代操作符是?do
?,在 2.13 小節(jié)介紹過。由于?do
?包含了隱式的?block
?及?tagbody
?,我們現(xiàn)在知道是可以在?do
?主體內(nèi)使用?return
?、?return-from
?以及?go
?。
2.13 節(jié)提到?do
?的第一個參數(shù)必須是說明變量規(guī)格的列表,列表可以是如下形式:
(variable initial update)
initial
?與?update
?形式是選擇性的。若?update
?形式忽略時,每次迭代時不會更新變量。若?initial
?形式也忽略時,變量會使用nil
?來初始化。
在 23 頁的例子中(譯注: 2.13 節(jié)),
(defun show-squares (start end)
(do ((i start (+ i 1)))
((> i end) 'done)
(format t "~A ~A~%" i (* i i))))
update
?形式引用到由?do
?所創(chuàng)造的變量。一般都是這么用。如果一個?do
?的?update
?形式,沒有至少引用到一個?do
?創(chuàng)建的變量時,反而很奇怪。
當同時更新超過一個變量時,問題來了,如果一個?update
?形式,引用到一個擁有自己的?update
?形式的變量時,它會被更新呢?或是獲得前一次迭代的值?使用?do
?的話,它獲得后者的值:
> (let ((x 'a))
(do ((x 1 (+ x 1))
(y x x))
((> x 5))
(format t "(~A ~A) " x y)))
(1 A) (2 1) (3 2) (4 3) (5 4)
NIL
每一次迭代時,?x
?獲得先前的值,加上一;?y
?也獲得?x
?的前一次數(shù)值。
但也有一個?do*
?,它有著和?let
?與?let*
?一樣的關(guān)系。任何?initial
?或?update
?形式可以參照到前一個子句的變量,并會獲得當下的值:
> (do* ((x 1 (+ x 1))
(y x x))
((> x 5))
(format t "(~A ~A) " x y))
(1 1) (2 2) (3 3) (4 4) (5 5)
NIL
除了?do
?與?do*
?之外,也有幾個特別用途的迭代操作符。要迭代一個列表的元素,我們可以使用?dolist
?:
> (dolist (x '(a b c d) 'done)
(format t "~A " x))
A B C D
DONE
當?shù)Y(jié)束時,初始列表內(nèi)的第三個表達式 (譯注:?done
?) ,會被求值并作為?dolist
?的返回值。缺省是?nil
?。
有著同樣的精神的是?dotimes
?,給定某個?n
?,將會從整數(shù)?0
?,迭代至?n-1
?:
(dotimes (x 5 x)
(format t "~A " x))
0 1 2 3 4
5
dolist
?與?dotimes?初始列表的第三個表達式皆可省略,省略時為?``nil
?。注意該表達式可引用到迭代過程中的變量。
(譯注:第三個表達式即上例之?x
?,可以省略,省略時?dotimes
?表達式的返回值為?nil
?。)
do 的重點 (THE POINT OF do)
在 “The Evolution of Lisp” 里,Steele 與 Garbriel 陳述了 do 的重點, 表達的實在太好了,值得整個在這里引用過來:
撇開爭論語法不談,有件事要說明的是,在任何一個編程語言中,一個循環(huán)若一次只能更新一個變量是毫無用處的。 幾乎在任何情況下,會有一個變量用來產(chǎn)生下個值,而另一個變量用來累積結(jié)果。如果循環(huán)語法只能產(chǎn)生變量, 那么累積結(jié)果就得借由賦值語句來“手動”實現(xiàn)…或有其他的副作用。具有多變量的 do 循環(huán),體現(xiàn)了產(chǎn)生與累積的本質(zhì)對稱性,允許可以無副作用地表達迭代過程:
(defun factorial (n)
(do ((j n (- j 1))
(f 1 (* j f)))
((= j 0) f)))
當然在 step 形式里實現(xiàn)所有的實際工作,一個沒有主體的 do 循環(huán)形式是較不尋常的。
函數(shù)?mapc
?和?mapcar
?很像,但不會?cons
?一個新列表作為返回值,所以使用的唯一理由是為了副作用。它們比?dolist
?來得靈活,因為可以同時遍歷多個列表:
> (mapc #'(lambda (x y)
(format t "~A ~A " x y))
'(hip flip slip)
'(hop flop slop))
HIP HOP FLIP FLOP SLIP SLOP
(HIP FLIP SLIP)
總是返回?mapc
?的第二個參數(shù)。
曾有人這么說,為了要強調(diào)函數(shù)式編程的重要性,每個 Lisp 表達式都返回一個值?,F(xiàn)在事情不是這么簡單了;在 Common Lisp 里,一個表達式可以返回零個或多個數(shù)值。最多可以返回幾個值取決于各家實現(xiàn),但至少可以返回 19 個值。
多值允許一個函數(shù)返回多件事情的計算結(jié)果,而不用構(gòu)造一個特定的結(jié)構(gòu)。舉例來說,內(nèi)置的?get-decoded-time
?返回 9 個數(shù)值來表示現(xiàn)在的時間:秒,分,時,日期,月,年,天,以及另外兩個數(shù)值。
多值也使得查詢函數(shù)可以分辨出?nil
?與查詢失敗的情況。這也是為什么?gethash
?返回兩個值。因為它使用第二個數(shù)值來指出成功還是失敗,我們可以在哈希表里儲存?nil
?,就像我們可以儲存別的數(shù)值那樣。
values
?函數(shù)返回多個數(shù)值。它一個不少地返回你作為數(shù)值所傳入的實參:
> (values 'a nil (+ 2 4))
A
NIL
6
如果一個?values
?表達式,是函數(shù)主體最后求值的表達式,它所返回的數(shù)值變成函數(shù)的返回值。多值可以原封不地通過任何數(shù)量的返回來傳遞:
> ((lambda () ((lambda () (values 1 2)))))
1
2
然而若只預(yù)期一個返回值時,第一個之外的值會被舍棄:
> (let ((x (values 1 2)))
x)
1
通過不帶實參使用?values
?,是可能不返回值的。在這個情況下,預(yù)期一個返回值的話,會獲得?nil
?:
> (values)
> (let ((x (values)))
x)
NIL
要接收多個數(shù)值,我們使用?multiple-value-bind
?:
> (multiple-value-bind (x y z) (values 1 2 3)
(list x y z))
(1 2 3)
> (multiple-value-bind (x y z) (values 1 2)
(list x y z))
(1 2 NIL)
如果變量的數(shù)量大于數(shù)值的數(shù)量,剩余的變量會是?nil
?。如果數(shù)值的數(shù)量大于變量的數(shù)量,多余的值會被舍棄。所以只想印出時間我們可以這么寫:
> (multiple-value-bind (s m h) (get-decoded-time)
(format t "~A:~A:~A" h m s))
"4:32:13"
你可以借由?multiple-value-call
?將多值作為實參傳給第二個函數(shù):
> (multiple-value-call #'+ (values 1 2 3))
6
還有一個函數(shù)是?multiple-value-list
?:
> (multiple-value-list (values 'a 'b 'c))
(A B C)
看起來像是使用?#'list
?作為第一個參數(shù)的來調(diào)用?multiple-value-call
?。
你可以使用?return
?在任何時候離開一個?block
?。有時候我們想要做更極端的事,在數(shù)個函數(shù)調(diào)用里將控制權(quán)轉(zhuǎn)移回來。要達成這件事,我們使用?catch
?與?throw
?。一個?catch
?表達式接受一個標簽(tag),標簽可以是任何類型的對象,伴隨著一個表達式主體:
(defun super ()
(catch 'abort
(sub)
(format t "We'll never see this.")))
(defun sub ()
(throw 'abort 99))
表達式依序求值,就像它們是在?progn
?里一樣。在這段代碼里的任何地方,一個帶有特定標簽的?throw
?會導(dǎo)致?catch
?表達式直接返回:
> (super)
99
一個帶有給定標簽的?throw
?,為了要到達匹配標簽的?catch
?,會將控制權(quán)轉(zhuǎn)移 (因此殺掉進程)給任何有標簽的?catch
?。如果沒有一個?catch
?符合欲匹配的標簽時,?throw
?會產(chǎn)生一個錯誤。
調(diào)用?error
?同時中斷了執(zhí)行,本來會將控制權(quán)轉(zhuǎn)移到調(diào)用樹(calling tree)的更高點,取而代之的是,它將控制權(quán)轉(zhuǎn)移給 Lisp 錯誤處理器(error handler)。通常會導(dǎo)致調(diào)用一個中斷循環(huán)(break loop)。以下是一個假定的 Common Lisp 實現(xiàn)可能會發(fā)生的事情:
> (progn
(error "Oops!")
(format t "After the error."))
Error: Oops!
Options: :abort, :backtrace
>>
譯注:2 個?>>
?顯示進入中斷循環(huán)了。
關(guān)于錯誤與狀態(tài)的更多訊息,參見 14.6 小節(jié)以及附錄 A。
有時候你想要防止代碼被?throw
?與?error
?打斷。借由使用?unwind-protect
?,可以確保像是前述的中斷,不會讓你的程序停在不一致的狀態(tài)。一個?unwind-protect
?接受任何數(shù)量的實參,并返回第一個實參的值。然而即便是第一個實參的求值被打斷時,剩下的表達式仍會被求值:
> (setf x 1)
1
> (catch 'abort
(unwind-protect
(throw 'abort 99)
(setf x 2)))
99
> x
2
在這里,即便?throw
?將控制權(quán)交回監(jiān)測的?catch
?,?unwind-protect
?確??刂茩?quán)移交時,第二個表達式有被求值。無論何時,一個確切的動作要伴隨著某種清理或重置時,?unwind-protect
?可能會派上用場。在 121 頁提到了一個例子。
在某些應(yīng)用里,能夠做日期的加減是很有用的 ── 舉例來說,能夠算出從 1997 年 12 月 17 日,六十天之后是 1998 年 2 月 15 日。在這個小節(jié)里,我們會編寫一個實用的工具來做日期運算。我們會將日期轉(zhuǎn)成整數(shù),起始點設(shè)置在 2000 年 1 月 1 日。我們會使用內(nèi)置的?+
?與?-
?函數(shù)來處理這些數(shù)字,而當我們轉(zhuǎn)換完畢時,再將結(jié)果轉(zhuǎn)回日期。
要將日期轉(zhuǎn)成數(shù)字,我們需要從日期的單位中,算出總天數(shù)有多少。舉例來說,2004 年 11 月 13 日的天數(shù)總和,是從起始點至 2004 年有多少天,加上從 2004 年到 2004 年 11 月有多少天,再加上 13 天。
有一個我們會需要的東西是,一張列出非潤年每月份有多少天的表格。我們可以使用 Lisp 來推敲出這個表格的內(nèi)容。我們從列出每月份的長度開始:
> (setf mon '(31 28 31 30 31 30 31 31 30 31 30 31))
(31 28 31 30 31 30 31 31 30 31 30 31)
我們可以通過應(yīng)用?+
?函數(shù)至這個列表來測試總長度:
> (apply #'+ mon)
365
現(xiàn)在如果我們反轉(zhuǎn)這個列表并使用?maplist
?來應(yīng)用?+
?函數(shù)至每下一個?cdr
?上,我們可以獲得從每個月份開始所累積的天數(shù):
> (setf nom (reverse mon))
(31 30 31 30 31 31 30 31 30 31 28 31)
> (setf sums (maplist #'(lambda (x)
(apply #'+ x))
nom))
(365 334 304 273 243 212 181 151 120 90 59 31)
這些數(shù)字體現(xiàn)了從二月一號開始已經(jīng)過了 31 天,從三月一號開始已經(jīng)過了 59 天……等等。
我們剛剛建立的這個列表,可以轉(zhuǎn)換成一個向量,見圖 5.1,轉(zhuǎn)換日期至整數(shù)的代碼。
(defconstant month
#(0 31 59 90 120 151 181 212 243 273 304 334 365))
(defconstant yzero 2000)
(defun leap? (y)
(and (zerop (mod y 4))
(or (zerop (mod y 400))
(not (zerop (mod y 100))))))
(defun date->num (d m y)
(+ (- d 1) (month-num m y) (year-num y)))
(defun month-num (m y)
(+ (svref month (- m 1))
(if (and (> m 2) (leap? y)) 1 0)))
(defun year-num (y)
(let ((d 0))
(if (>= y yzero)
(dotimes (i (- y yzero) d)
(incf d (year-days (+ yzero i))))
(dotimes (i (- yzero y) (- d))
(incf d (year-days (+ y i)))))))
(defun year-days (y) (if (leap? y) 366 365))
圖 5.1 日期運算:轉(zhuǎn)換日期至數(shù)字
典型 Lisp 程序的生命周期有四個階段:先寫好,然后讀入,接著編譯,最后執(zhí)行。有件 Lisp 非常獨特的事情之一是,在這四個階段時, Lisp 一直都在那里??梢栽谀愕某绦蚓幾g (參見 10.2 小節(jié))或讀入時 (參見 14.3 小節(jié)) 來調(diào)用 Lisp。我們推導(dǎo)出?month
?的過程演示了,如何在撰寫一個程序時使用 Lisp。
效率通常只跟第四個階段有關(guān)系,運行期(run-time)。在前三個階段,你可以隨意的使用列表擁有的威力與靈活性,而不需要擔(dān)心效率。
若你使用圖 5.1 的代碼來造一個時光機器(time machine),當你抵達時,人們大概會不同意你的日期。即使是相對近的現(xiàn)在,歐洲的日期也曾有過偏移,因為人們會獲得更精準的每年有多長的概念。在說英語的國家,最后一次的不連續(xù)性出現(xiàn)在 1752 年,日期從 9 月 2 日跳到 9 月 14 日。
每年有幾天取決于該年是否是潤年。如果該年可以被四整除,我們說該年是潤年,除非該年可以被 100 整除,則該年非潤年 ── 而要是它可以被 400 整除,則又是潤年。所以 1904 年是潤年,1900 年不是,而 1600 年是。
要決定某個數(shù)是否可以被另個數(shù)整除,我們使用函數(shù)?mod
?,返回相除后的余數(shù):
> (mod 23 5)
3
> (mod 25 5)
0
如果第一個實參除以第二個實參的余數(shù)為 0,則第一個實參是可以被第二個實參整除的。函數(shù)?leap?
?使用了這個方法,來決定它的實參是否是一個潤年:
> (mapcar #'leap? '(1904 1900 1600))
(T NIL T)
我們用來轉(zhuǎn)換日期至整數(shù)的函數(shù)是?date->num
?。它返回日期中每個單位的天數(shù)總和。要找到從某月份開始的天數(shù)和,我們調(diào)用month-num
?,它在?month
?中查詢天數(shù),如果是在潤年的二月之后,則加一。
要找到從某年開始的天數(shù)和,?date->num
?調(diào)用?year-num
?,它返回某年一月一日相對于起始點(2000.01.01)所代表的天數(shù)。這個函數(shù)的工作方式是從傳入的實參?y
?年開始,朝著起始年(2000)往上或往下數(shù)。
(defun num->date (n)
(multiple-value-bind (y left) (num-year n)
(multiple-value-bind (m d) (num-month left y)
(values d m y))))
(defun num-year (n)
(if (< n 0)
(do* ((y (- yzero 1) (- y 1))
(d (- (year-days y)) (- d (year-days y))))
((<= d n) (values y (- n d))))
(do* ((y yzero (+ y 1))
(prev 0 d)
(d (year-days y) (+ d (year-days y))))
((> d n) (values y (- n prev))))))
(defun num-month (n y)
(if (leap? y)
(cond ((= n 59) (values 2 29))
((> n 59) (nmon (- n 1)))
(t (nmon n)))
(nmon n)))
(defun nmon (n)
(let ((m (position n month :test #'<)))
(values m (+ 1 (- n (svref month (- m 1)))))))
(defun date+ (d m y n)
(num->date (+ (date->num d m y) n)))
圖 5.2 日期運算:轉(zhuǎn)換數(shù)字至日期
圖 5.2 展示了代碼的下半部份。函數(shù)?num->date
?將整數(shù)轉(zhuǎn)換回日期。它調(diào)用了?num-year
?函數(shù),以日期的格式返回年,以及剩余的天數(shù)。再將剩余的天數(shù)傳給?num-month
?,分解出月與日。
和?year-num
?相同,?num-year
?從起始年往上或下數(shù),一次數(shù)一年。并持續(xù)累積天數(shù),直到它獲得一個絕對值大于或等于?n
?的數(shù)。如果它往下數(shù),那么它可以返回當前迭代中的數(shù)值。不然它會超過年份,然后必須返回前次迭代的數(shù)值。這也是為什么要使用?prev
?,prev
?在每次迭代時會存入?days
?前次迭代的數(shù)值。
函數(shù)?num-month
?以及它的子程序(subroutine)?nmon
?的行為像是相反地?month-num
?。他們從常數(shù)向量?month
?的數(shù)值到位置,然而month-num
?從位置到數(shù)值。
圖 5.2 的前兩個函數(shù)可以合而為一。與其返回數(shù)值給另一個函數(shù),?num-year
?可以直接調(diào)用?num-month
??,F(xiàn)在分成兩部分的代碼,比較容易做交互測試,但是現(xiàn)在它可以工作了,下一步或許是把它合而為一。
有了?date->num
?與?num->date
?,日期運算是很簡單的。我們在?date+
?里使用它們,可以從特定的日期做加減。如果我們想透過date+
?來知道 1997 年 12 月 17 日六十天之后的日期:
> (multiple-value-list (date+ 17 12 1997 60))
(15 2 1998)
我們得到,1998 年 2 月 15 日。
progn
?;允許返回的?block
?;以及允許?goto
?的?tagbody
?。很多內(nèi)置的操作符隱含在區(qū)塊里。if
?來定義。let
?與?let*
?,并使同樣的表達式不被求值 2 次。(a) (let ((x (car y)))
(cons x x))
(b) (let* ((w (car x))
(y (+ w z)))
(cons w y))
cond
?重寫 29 頁的?mystery
?函數(shù)。(譯注: 第二章的練習(xí)第 5 題的 (b) 部分)case
?與?svref
?重寫?month-num
?(圖 5.1)。x
?之前的對象:> (precedes #\a "abracadabra")
(#\c #\d #\r)
> (intersperse '- '(a b c d))
(A - B - C - D)
(a) 遞歸
(b) do
(c) mapc 與 return
(a) 使用 catch 與 throw 來變更程序,使其找到第一個完整路徑時,直接返回它。
(b) 重寫一個做到同樣事情的程序,但不使用 catch 與 throw。
更多建議: