第五章:控制流

2018-02-24 15:51 更新

2.2 節(jié)介紹過(guò) Common Lisp 的求值規(guī)則,現(xiàn)在你應(yīng)該很熟悉了。本章的操作符都有一個(gè)共同點(diǎn),就是它們都違反了求值規(guī)則。這些操作符讓你決定在程序當(dāng)中何時(shí)要求值。如果普通的函數(shù)調(diào)用是 Lisp 程序的樹(shù)葉的話(huà),那這些操作符就是連結(jié)樹(shù)葉的樹(shù)枝。

5.1 區(qū)塊 (Blocks)

Common Lisp 有三個(gè)構(gòu)造區(qū)塊(block)的基本操作符:?progn?、?block?以及?tagbody?。我們已經(jīng)看過(guò)?progn?了。在?progn?主體中的表達(dá)式會(huì)依序求值,并返回最后一個(gè)表達(dá)式的值:

> (progn
    (format t "a")
    (format t "b")
    (+ 11 12))
ab
23

由于只返回最后一個(gè)表達(dá)式的值,代表著使用?progn?(或任何區(qū)塊)涵蓋了副作用。

一個(gè)?block?像是帶有名字及緊急出口的?progn?。第一個(gè)實(shí)參應(yīng)為符號(hào)。這成為了區(qū)塊的名字。在主體中的任何地方,可以停止求值,并通過(guò)使用?return-from?指定區(qū)塊的名字,來(lái)立即返回?cái)?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)雅地退出。第二個(gè)傳給?return-from?的實(shí)參,用來(lái)作為以第一個(gè)實(shí)參為名的區(qū)塊的返回值。在?return-from?之后的表達(dá)式不會(huì)被求值。

也有一個(gè)?return?宏,它把傳入的參數(shù)當(dāng)做封閉區(qū)塊?nil?的返回值:

> (block nil
    (return 27))
27

許多接受一個(gè)表達(dá)式主體的 Common Lisp 操作符,皆隱含在一個(gè)叫做?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ù)主體,都隱含在一個(gè)與函數(shù)同名的區(qū)塊,所以你可以:

(defun foo ()
  (return-from foo 27))

在一個(gè)顯式或隱式的?block?外,不論是?return-from?或?return?都不會(huì)工作。

使用?return-from?,我們可以寫(xiě)出一個(gè)更好的?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 頁(yè)的版本在構(gòu)造整數(shù)之前,需檢查所有的字符。現(xiàn)在兩個(gè)步驟可以結(jié)合,因?yàn)槿绻龅椒菙?shù)字的字符時(shí),我們可以舍棄計(jì)算結(jié)果。出現(xiàn)在主體的原子(atom)被解讀為標(biāo)簽(labels);把這樣的標(biāo)簽傳給?go?,會(huì)把控制權(quán)交給標(biāo)簽后的表達(dá)式。以下是一個(gè)非常丑的程序片段,用來(lái)印出一至十的數(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

這個(gè)操作符主要用來(lái)實(shí)現(xiàn)其它的操作符,不是一般會(huì)用到的操作符。大多數(shù)迭代操作符都隱含在一個(gè)?tagbody?,所以是可能可以在主體里(雖然很少想要)使用標(biāo)簽及?go?。

如何決定要使用哪一種區(qū)塊建構(gòu)子呢(block construct)?幾乎任何時(shí)候,你會(huì)使用?progn?。如果你想要突然退出的話(huà),使用block?來(lái)取代。多數(shù)程序員永遠(yuǎn)不會(huì)顯式地使用?tagbody?。

5.2 語(yǔ)境 (Context)

另一個(gè)我們用來(lái)區(qū)分表達(dá)式的操作符是?let?。它接受一個(gè)代碼主體,但允許我們?cè)谥黧w內(nèi)設(shè)置新變量:

> (let ((x 7)
        (y 2))
    (format t "Number")
    (+ x y))
Number
9

一個(gè)像是?let?的操作符,創(chuàng)造出一個(gè)新的詞法語(yǔ)境(lexical context)。在這個(gè)語(yǔ)境里有兩個(gè)新變量,然而在外部語(yǔ)境的變量也因此變得不可視了。

概念上說(shuō),一個(gè)?let?表達(dá)式等同于函數(shù)調(diào)用。在 2.14 節(jié)證明過(guò),函數(shù)可以用名字來(lái)引用,也可以通過(guò)使用一個(gè) lambda 表達(dá)式從字面上來(lái)引用。由于 lambda 表達(dá)式是函數(shù)的名字,我們可以像使用函數(shù)名那樣,把 lambda 表達(dá)式作為函數(shù)調(diào)用的第一個(gè)實(shí)參:

> ((lambda (x) (+ x 1)) 3)
4

前述的?let?表達(dá)式,實(shí)際上等同于:

((lambda (x y)
   (format t "Number")
   (+ x y))
 7
 2)

如果有關(guān)于?let?的任何問(wèn)題,應(yīng)該是如何把責(zé)任交給?lambda?,因?yàn)檫M(jìn)入一個(gè)?let?等同于執(zhí)行一個(gè)函數(shù)調(diào)用。

這個(gè)模型清楚的告訴我們,由?let?創(chuàng)造的變量的值,不能依賴(lài)其它由同一個(gè)?let?所創(chuàng)造的變量。舉例來(lái)說(shuō),如果我們?cè)囍?/p>

(let ((x 2)
      (y (+ x 1)))
  (+ x y))

在?(+?x?1)?中的?x?不是前一行所設(shè)置的值,因?yàn)檎麄€(gè)表達(dá)式等同于:

((lambda (x y) (+ x y)) 2
                        (+ x 1))

這里明顯看到?(+?x?1)?作為實(shí)參傳給函數(shù),不能引用函數(shù)內(nèi)的形參?x?。

所以如果你真的想要新變量的值,依賴(lài)同一個(gè)表達(dá)式所設(shè)立的另一個(gè)變量?在這個(gè)情況下,使用一個(gè)變形版本?let*?:

> (let* ((x 1)
         (y (+ x 1)))
    (+ x y))
3

一個(gè)?let*?功能上等同于一系列嵌套的?let?。這個(gè)特別的例子等同于:

(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?。其接受單一變量,一個(gè)模式 (pattern) ── 一個(gè)或多個(gè)變量所構(gòu)成的樹(shù) ── 并將它們與某個(gè)實(shí)際的樹(shù)所對(duì)應(yīng)的部份做綁定。舉例來(lái)說(shuō):

> (destructuring-bind (w (x y) . z) '(a (b c) d e)
    (list w x y z))
(A B C (D E))

若給定的樹(shù)(第二個(gè)實(shí)參)沒(méi)有與模式匹配(第一個(gè)參數(shù))時(shí),會(huì)產(chǎn)生錯(cuò)誤。

5.3 條件 (Conditionals)

最簡(jiǎn)單的條件式是?if?;其余的條件式都是基于?if?所構(gòu)造的。第二簡(jiǎn)單的條件式是?when?,它接受一個(gè)測(cè)試表達(dá)式(test expression)與一個(gè)代碼主體。若測(cè)試表達(dá)式求值返回真時(shí),則對(duì)主體求值。所以

(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?;它接受相同的實(shí)參,但僅在測(cè)試表達(dá)式返回假時(shí),才對(duì)主體求值。

所有條件式的母體 (從正反兩面看) 是?cond?,?cond?有兩個(gè)新的優(yōu)點(diǎn):允許多個(gè)條件判斷,與每個(gè)條件相關(guān)的代碼隱含在?progn?里。cond?預(yù)期在我們需要使用嵌套?if?的情況下使用。 舉例來(lái)說(shuō),這個(gè)偽 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)))))

事實(shí)上,Common Lisp 實(shí)現(xiàn)大概會(huì)把?cond?翻譯成?if?的形式。

總得來(lái)說(shuō)呢,?cond?接受零個(gè)或多個(gè)實(shí)參。每一個(gè)實(shí)參必須是一個(gè)具有條件式,伴隨著零個(gè)或多個(gè)表達(dá)式的列表。當(dāng)?cond?表達(dá)式被求值時(shí),測(cè)試條件式依序求值,直到某個(gè)測(cè)試條件式返回真才停止。當(dāng)返回真時(shí),與其相關(guān)聯(lián)的表達(dá)式會(huì)被依序求值,而最后一個(gè)返回的數(shù)值,會(huì)作為?cond?的返回值。如果符合的條件式之后沒(méi)有表達(dá)式的話(huà):

> (cond (99))
99

則會(huì)返回條件式的值。

由于?cond?子句的?t?條件永遠(yuǎn)成立,通常我們把它放在最后,作為缺省的條件式。如果沒(méi)有子句符合時(shí),則?cond?返回?nil?,但利用nil?作為返回值是一種很差的風(fēng)格 (這種問(wèn)題可能發(fā)生的例子,請(qǐng)看 292 頁(yè))。譯注:?Appendix A, unexpected nil?小節(jié)。

當(dāng)你想要把一個(gè)數(shù)值與一系列的常量比較時(shí),有?case?可以用。我們可以使用?case?來(lái)定義一個(gè)函數(shù),返回每個(gè)月份中的天數(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")))

一個(gè)?case?表達(dá)式由一個(gè)實(shí)參開(kāi)始,此實(shí)參會(huì)被拿來(lái)與每個(gè)子句的鍵值做比較。接著是零個(gè)或多個(gè)子句,每個(gè)子句由一個(gè)或一串鍵值開(kāi)始,跟隨著零個(gè)或多個(gè)表達(dá)式。鍵值被視為常量;它們不會(huì)被求值。第一個(gè)參數(shù)的值被拿來(lái)與子句中的鍵值做比較 (使用?eql?)。如果匹配時(shí),子句剩余的表達(dá)式會(huì)被求值,并將最后一個(gè)求值作為?case?的返回值。

缺省子句的鍵值可以是?t?或?otherwise?。如果沒(méi)有子句符合時(shí),或是子句只包含鍵值時(shí),

> (case 99 (99))
NIL

則?case?返回?nil?。

typecase?宏與?case?相似,除了每個(gè)子句中的鍵值應(yīng)為類(lèi)型修飾符 (type specifiers),以及第一個(gè)實(shí)參與鍵值比較的函數(shù)使用typep?而不是?eql?(一個(gè)?typecase?的例子在 107 頁(yè))。?譯注: 6.5 小節(jié)。

5.4 迭代 (Iteration)

最基本的迭代操作符是?do?,在 2.13 小節(jié)介紹過(guò)。由于?do?包含了隱式的?block?及?tagbody?,我們現(xiàn)在知道是可以在?do?主體內(nèi)使用?return?、?return-from?以及?go?。

2.13 節(jié)提到?do?的第一個(gè)參數(shù)必須是說(shuō)明變量規(guī)格的列表,列表可以是如下形式:

(variable  initial  update)

initial?與?update?形式是選擇性的。若?update?形式忽略時(shí),每次迭代時(shí)不會(huì)更新變量。若?initial?形式也忽略時(shí),變量會(huì)使用nil?來(lái)初始化。

在 23 頁(yè)的例子中(譯注: 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)造的變量。一般都是這么用。如果一個(gè)?do?的?update?形式,沒(méi)有至少引用到一個(gè)?do?創(chuàng)建的變量時(shí),反而很奇怪。

當(dāng)同時(shí)更新超過(guò)一個(gè)變量時(shí),問(wèn)題來(lái)了,如果一個(gè)?update?形式,引用到一個(gè)擁有自己的?update?形式的變量時(shí),它會(huì)被更新呢?或是獲得前一次迭代的值?使用?do?的話(huà),它獲得后者的值:

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

每一次迭代時(shí),?x?獲得先前的值,加上一;?y?也獲得?x?的前一次數(shù)值。

但也有一個(gè)?do*?,它有著和?let?與?let*?一樣的關(guān)系。任何?initial?或?update?形式可以參照到前一個(gè)子句的變量,并會(huì)獲得當(dāng)下的值:

> (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*?之外,也有幾個(gè)特別用途的迭代操作符。要迭代一個(gè)列表的元素,我們可以使用?dolist?:

> (dolist (x '(a b c d) 'done)
    (format t "~A " x))
A B C D
DONE

當(dāng)?shù)Y(jié)束時(shí),初始列表內(nèi)的第三個(gè)表達(dá)式 (譯注:?done?) ,會(huì)被求值并作為?dolist?的返回值。缺省是?nil?。

有著同樣的精神的是?dotimes?,給定某個(gè)?n?,將會(huì)從整數(shù)?0?,迭代至?n-1?:

(dotimes (x 5 x)
  (format t "~A " x))
0 1 2 3 4
5

dolist?與?dotimes?初始列表的第三個(gè)表達(dá)式皆可省略,省略時(shí)為?``nil?。注意該表達(dá)式可引用到迭代過(guò)程中的變量。

(譯注:第三個(gè)表達(dá)式即上例之?x?,可以省略,省略時(shí)?dotimes?表達(dá)式的返回值為?nil?。)

do 的重點(diǎn) (THE POINT OF do)

在 “The Evolution of Lisp” 里,Steele 與 Garbriel 陳述了 do 的重點(diǎn), 表達(dá)的實(shí)在太好了,值得整個(gè)在這里引用過(guò)來(lái):

撇開(kāi)爭(zhēng)論語(yǔ)法不談,有件事要說(shuō)明的是,在任何一個(gè)編程語(yǔ)言中,一個(gè)循環(huán)若一次只能更新一個(gè)變量是毫無(wú)用處的。 幾乎在任何情況下,會(huì)有一個(gè)變量用來(lái)產(chǎn)生下個(gè)值,而另一個(gè)變量用來(lái)累積結(jié)果。如果循環(huán)語(yǔ)法只能產(chǎn)生變量, 那么累積結(jié)果就得借由賦值語(yǔ)句來(lái)“手動(dòng)”實(shí)現(xiàn)…或有其他的副作用。具有多變量的 do 循環(huán),體現(xiàn)了產(chǎn)生與累積的本質(zhì)對(duì)稱(chēng)性,允許可以無(wú)副作用地表達(dá)迭代過(guò)程:

(defun factorial (n)
  (do ((j n (- j 1))
       (f 1 (* j f)))
    ((= j 0) f)))

當(dāng)然在 step 形式里實(shí)現(xiàn)所有的實(shí)際工作,一個(gè)沒(méi)有主體的 do 循環(huán)形式是較不尋常的。

函數(shù)?mapc?和?mapcar?很像,但不會(huì)?cons?一個(gè)新列表作為返回值,所以使用的唯一理由是為了副作用。它們比?dolist?來(lái)得靈活,因?yàn)榭梢酝瑫r(shí)遍歷多個(gè)列表:

> (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?的第二個(gè)參數(shù)。

5.5 多值 (Multiple Values)

曾有人這么說(shuō),為了要強(qiáng)調(diào)函數(shù)式編程的重要性,每個(gè) Lisp 表達(dá)式都返回一個(gè)值?,F(xiàn)在事情不是這么簡(jiǎn)單了;在 Common Lisp 里,一個(gè)表達(dá)式可以返回零個(gè)或多個(gè)數(shù)值。最多可以返回幾個(gè)值取決于各家實(shí)現(xiàn),但至少可以返回 19 個(gè)值。

多值允許一個(gè)函數(shù)返回多件事情的計(jì)算結(jié)果,而不用構(gòu)造一個(gè)特定的結(jié)構(gòu)。舉例來(lái)說(shuō),內(nèi)置的?get-decoded-time?返回 9 個(gè)數(shù)值來(lái)表示現(xiàn)在的時(shí)間:秒,分,時(shí),日期,月,年,天,以及另外兩個(gè)數(shù)值。

多值也使得查詢(xún)函數(shù)可以分辨出?nil?與查詢(xún)失敗的情況。這也是為什么?gethash?返回兩個(gè)值。因?yàn)樗褂玫诙€(gè)數(shù)值來(lái)指出成功還是失敗,我們可以在哈希表里儲(chǔ)存?nil?,就像我們可以?xún)?chǔ)存別的數(shù)值那樣。

values?函數(shù)返回多個(gè)數(shù)值。它一個(gè)不少地返回你作為數(shù)值所傳入的實(shí)參:

> (values 'a nil (+ 2 4))
A
NIL
6

如果一個(gè)?values?表達(dá)式,是函數(shù)主體最后求值的表達(dá)式,它所返回的數(shù)值變成函數(shù)的返回值。多值可以原封不地通過(guò)任何數(shù)量的返回來(lái)傳遞:

> ((lambda () ((lambda () (values 1 2)))))
1
2

然而若只預(yù)期一個(gè)返回值時(shí),第一個(gè)之外的值會(huì)被舍棄:

> (let ((x (values 1 2)))
    x)
1

通過(guò)不帶實(shí)參使用?values?,是可能不返回值的。在這個(gè)情況下,預(yù)期一個(gè)返回值的話(huà),會(huì)獲得?nil?:

> (values)
> (let ((x (values)))
    x)
NIL

要接收多個(gè)數(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ù)量,剩余的變量會(huì)是?nil?。如果數(shù)值的數(shù)量大于變量的數(shù)量,多余的值會(huì)被舍棄。所以只想印出時(shí)間我們可以這么寫(xiě):

> (multiple-value-bind (s m h) (get-decoded-time)
    (format t "~A:~A:~A" h m s))
"4:32:13"

你可以借由?multiple-value-call?將多值作為實(shí)參傳給第二個(gè)函數(shù):

> (multiple-value-call #'+ (values 1 2 3))
6

還有一個(gè)函數(shù)是?multiple-value-list?:

> (multiple-value-list (values 'a 'b 'c))
(A B C)

看起來(lái)像是使用?#'list?作為第一個(gè)參數(shù)的來(lái)調(diào)用?multiple-value-call?。

5.6 中止 (Aborts)

你可以使用?return?在任何時(shí)候離開(kāi)一個(gè)?block?。有時(shí)候我們想要做更極端的事,在數(shù)個(gè)函數(shù)調(diào)用里將控制權(quán)轉(zhuǎn)移回來(lái)。要達(dá)成這件事,我們使用?catch?與?throw?。一個(gè)?catch?表達(dá)式接受一個(gè)標(biāo)簽(tag),標(biāo)簽可以是任何類(lèi)型的對(duì)象,伴隨著一個(gè)表達(dá)式主體:

(defun super ()
  (catch 'abort
    (sub)
    (format t "We'll never see this.")))

(defun sub ()
  (throw 'abort 99))

表達(dá)式依序求值,就像它們是在?progn?里一樣。在這段代碼里的任何地方,一個(gè)帶有特定標(biāo)簽的?throw?會(huì)導(dǎo)致?catch?表達(dá)式直接返回:

> (super)
99

一個(gè)帶有給定標(biāo)簽的?throw?,為了要到達(dá)匹配標(biāo)簽的?catch?,會(huì)將控制權(quán)轉(zhuǎn)移 (因此殺掉進(jìn)程)給任何有標(biāo)簽的?catch?。如果沒(méi)有一個(gè)?catch?符合欲匹配的標(biāo)簽時(shí),?throw?會(huì)產(chǎn)生一個(gè)錯(cuò)誤。

調(diào)用?error?同時(shí)中斷了執(zhí)行,本來(lái)會(huì)將控制權(quán)轉(zhuǎn)移到調(diào)用樹(shù)(calling tree)的更高點(diǎn),取而代之的是,它將控制權(quán)轉(zhuǎn)移給 Lisp 錯(cuò)誤處理器(error handler)。通常會(huì)導(dǎo)致調(diào)用一個(gè)中斷循環(huán)(break loop)。以下是一個(gè)假定的 Common Lisp 實(shí)現(xiàn)可能會(huì)發(fā)生的事情:

> (progn
    (error "Oops!")
    (format t "After the error."))
Error: Oops!
       Options: :abort, :backtrace
>>

譯注:2 個(gè)?>>?顯示進(jìn)入中斷循環(huán)了。

關(guān)于錯(cuò)誤與狀態(tài)的更多訊息,參見(jiàn) 14.6 小節(jié)以及附錄 A。

有時(shí)候你想要防止代碼被?throw?與?error?打斷。借由使用?unwind-protect?,可以確保像是前述的中斷,不會(huì)讓你的程序停在不一致的狀態(tài)。一個(gè)?unwind-protect?接受任何數(shù)量的實(shí)參,并返回第一個(gè)實(shí)參的值。然而即便是第一個(gè)實(shí)參的求值被打斷時(shí),剩下的表達(dá)式仍會(huì)被求值:

> (setf x 1)
1
> (catch 'abort
    (unwind-protect
      (throw 'abort 99)
      (setf x 2)))
99
> x
2

在這里,即便?throw?將控制權(quán)交回監(jiān)測(cè)的?catch?,?unwind-protect?確??刂茩?quán)移交時(shí),第二個(gè)表達(dá)式有被求值。無(wú)論何時(shí),一個(gè)確切的動(dòng)作要伴隨著某種清理或重置時(shí),?unwind-protect?可能會(huì)派上用場(chǎng)。在 121 頁(yè)提到了一個(gè)例子。

5.7 示例:日期運(yùn)算 (Example: Date Arithmetic)

在某些應(yīng)用里,能夠做日期的加減是很有用的 ── 舉例來(lái)說(shuō),能夠算出從 1997 年 12 月 17 日,六十天之后是 1998 年 2 月 15 日。在這個(gè)小節(jié)里,我們會(huì)編寫(xiě)一個(gè)實(shí)用的工具來(lái)做日期運(yùn)算。我們會(huì)將日期轉(zhuǎn)成整數(shù),起始點(diǎn)設(shè)置在 2000 年 1 月 1 日。我們會(huì)使用內(nèi)置的?+?與?-?函數(shù)來(lái)處理這些數(shù)字,而當(dāng)我們轉(zhuǎn)換完畢時(shí),再將結(jié)果轉(zhuǎn)回日期。

要將日期轉(zhuǎn)成數(shù)字,我們需要從日期的單位中,算出總天數(shù)有多少。舉例來(lái)說(shuō),2004 年 11 月 13 日的天數(shù)總和,是從起始點(diǎn)至 2004 年有多少天,加上從 2004 年到 2004 年 11 月有多少天,再加上 13 天。

有一個(gè)我們會(huì)需要的東西是,一張列出非潤(rùn)年每月份有多少天的表格。我們可以使用 Lisp 來(lái)推敲出這個(gè)表格的內(nèi)容。我們從列出每月份的長(zhǎng)度開(kā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)

我們可以通過(guò)應(yīng)用?+?函數(shù)至這個(gè)列表來(lái)測(cè)試總長(zhǎng)度:

> (apply #'+ mon)
365

現(xiàn)在如果我們反轉(zhuǎn)這個(gè)列表并使用?maplist?來(lái)應(yīng)用?+?函數(shù)至每下一個(gè)?cdr?上,我們可以獲得從每個(gè)月份開(kāi)始所累積的天數(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)了從二月一號(hào)開(kāi)始已經(jīng)過(guò)了 31 天,從三月一號(hào)開(kāi)始已經(jīng)過(guò)了 59 天……等等。

我們剛剛建立的這個(gè)列表,可以轉(zhuǎn)換成一個(gè)向量,見(jià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 日期運(yùn)算:轉(zhuǎn)換日期至數(shù)字

典型 Lisp 程序的生命周期有四個(gè)階段:先寫(xiě)好,然后讀入,接著編譯,最后執(zhí)行。有件 Lisp 非常獨(dú)特的事情之一是,在這四個(gè)階段時(shí), Lisp 一直都在那里??梢栽谀愕某绦蚓幾g (參見(jiàn) 10.2 小節(jié))或讀入時(shí) (參見(jiàn) 14.3 小節(jié)) 來(lái)調(diào)用 Lisp。我們推導(dǎo)出?month?的過(guò)程演示了,如何在撰寫(xiě)一個(gè)程序時(shí)使用 Lisp。

效率通常只跟第四個(gè)階段有關(guān)系,運(yùn)行期(run-time)。在前三個(gè)階段,你可以隨意的使用列表?yè)碛械耐εc靈活性,而不需要擔(dān)心效率。

若你使用圖 5.1 的代碼來(lái)造一個(gè)時(shí)光機(jī)器(time machine),當(dāng)你抵達(dá)時(shí),人們大概會(huì)不同意你的日期。即使是相對(duì)近的現(xiàn)在,歐洲的日期也曾有過(guò)偏移,因?yàn)槿藗儠?huì)獲得更精準(zhǔn)的每年有多長(zhǎng)的概念。在說(shuō)英語(yǔ)的國(guó)家,最后一次的不連續(xù)性出現(xiàn)在 1752 年,日期從 9 月 2 日跳到 9 月 14 日。

每年有幾天取決于該年是否是潤(rùn)年。如果該年可以被四整除,我們說(shuō)該年是潤(rùn)年,除非該年可以被 100 整除,則該年非潤(rùn)年 ── 而要是它可以被 400 整除,則又是潤(rùn)年。所以 1904 年是潤(rùn)年,1900 年不是,而 1600 年是。

要決定某個(gè)數(shù)是否可以被另個(gè)數(shù)整除,我們使用函數(shù)?mod?,返回相除后的余數(shù):

> (mod 23 5)
3
> (mod 25 5)
0

如果第一個(gè)實(shí)參除以第二個(gè)實(shí)參的余數(shù)為 0,則第一個(gè)實(shí)參是可以被第二個(gè)實(shí)參整除的。函數(shù)?leap??使用了這個(gè)方法,來(lái)決定它的實(shí)參是否是一個(gè)潤(rùn)年:

> (mapcar #'leap? '(1904 1900 1600))
(T NIL T)

我們用來(lái)轉(zhuǎn)換日期至整數(shù)的函數(shù)是?date->num?。它返回日期中每個(gè)單位的天數(shù)總和。要找到從某月份開(kāi)始的天數(shù)和,我們調(diào)用month-num?,它在?month?中查詢(xún)天數(shù),如果是在潤(rùn)年的二月之后,則加一。

要找到從某年開(kāi)始的天數(shù)和,?date->num?調(diào)用?year-num?,它返回某年一月一日相對(duì)于起始點(diǎn)(2000.01.01)所代表的天數(shù)。這個(gè)函數(shù)的工作方式是從傳入的實(shí)參?y?年開(kāi)始,朝著起始年(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 日期運(yùn)算:轉(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ù),直到它獲得一個(gè)絕對(duì)值大于或等于?n?的數(shù)。如果它往下數(shù),那么它可以返回當(dāng)前迭代中的數(shù)值。不然它會(huì)超過(guò)年份,然后必須返回前次迭代的數(shù)值。這也是為什么要使用?prev?,prev?在每次迭代時(shí)會(huì)存入?days?前次迭代的數(shù)值。

函數(shù)?num-month?以及它的子程序(subroutine)?nmon?的行為像是相反地?month-num?。他們從常數(shù)向量?month?的數(shù)值到位置,然而month-num?從位置到數(shù)值。

圖 5.2 的前兩個(gè)函數(shù)可以合而為一。與其返回?cái)?shù)值給另一個(gè)函數(shù),?num-year?可以直接調(diào)用?num-month??,F(xiàn)在分成兩部分的代碼,比較容易做交互測(cè)試,但是現(xiàn)在它可以工作了,下一步或許是把它合而為一。

有了?date->num?與?num->date?,日期運(yùn)算是很簡(jiǎn)單的。我們?cè)?date+?里使用它們,可以從特定的日期做加減。如果我們想透過(guò)date+?來(lái)知道 1997 年 12 月 17 日六十天之后的日期:

> (multiple-value-list (date+ 17 12 1997 60))
(15 2 1998)

我們得到,1998 年 2 月 15 日。

Chapter 5 總結(jié) (Summary)

  1. Common Lisp 有三個(gè)基本的區(qū)塊建構(gòu)子:?progn?;允許返回的?block?;以及允許?goto?的?tagbody?。很多內(nèi)置的操作符隱含在區(qū)塊里。
  2. 進(jìn)入一個(gè)新的詞法語(yǔ)境,概念上等同于函數(shù)調(diào)用。
  3. Common Lisp 提供了適合不同情況的條件式。每個(gè)都可以使用?if?來(lái)定義。
  4. 有數(shù)個(gè)相似迭代操作符的變種。
  5. 表達(dá)式可以返回多個(gè)數(shù)值。
  6. 計(jì)算過(guò)程可以被中斷以及保護(hù),保護(hù)可使其免于中斷所造成的后果。

Chapter 5 練習(xí) (Exercises)

  1. 將下列表達(dá)式翻譯成沒(méi)有使用?let?與?let*?,并使同樣的表達(dá)式不被求值 2 次。
(a) (let ((x (car y)))
      (cons x x))
(b) (let* ((w (car x))
           (y (+ w z)))
      (cons w y))
  1. 使用?cond?重寫(xiě) 29 頁(yè)的?mystery?函數(shù)。(譯注: 第二章的練習(xí)第 5 題的 (b) 部分)
  2. 定義一個(gè)返回其實(shí)參平方的函數(shù),而當(dāng)實(shí)參是一個(gè)正整數(shù)且小于等于 5 時(shí),不要計(jì)算其平方。
  3. 使用?case?與?svref?重寫(xiě)?month-num?(圖 5.1)。
  4. 定義一個(gè)迭代與遞歸版本的函數(shù),接受一個(gè)對(duì)象 x 與向量 v ,并返回一個(gè)列表,包含了向量 v 當(dāng)中,所有直接在?x?之前的對(duì)象:
> (precedes #\a "abracadabra")
(#\c #\d #\r)
  1. 定義一個(gè)迭代與遞歸版本的函數(shù),接受一個(gè)對(duì)象與列表,并返回一個(gè)新的列表,在原本列表的對(duì)象之間加上傳入的對(duì)象:
> (intersperse '- '(a b c d))
(A - B - C - D)
  1. 定義一個(gè)接受一系列數(shù)字的函數(shù),并在若且唯若每一對(duì)(pair)數(shù)字的差為一時(shí),返回真,使用
(a) 遞歸
(b) do
(c) mapc 與 return
  1. 定義一個(gè)單遞歸函數(shù),返回兩個(gè)值,分別是向量的最大與最小值。
  2. 圖 3.12 的程序在找到一個(gè)完整的路徑時(shí),仍持續(xù)遍歷佇列。在搜索范圍大時(shí),這可能會(huì)產(chǎn)生問(wèn)題。
(a) 使用 catch 與 throw 來(lái)變更程序,使其找到第一個(gè)完整路徑時(shí),直接返回它。
(b) 重寫(xiě)一個(gè)做到同樣事情的程序,但不使用 catch 與 throw。
以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)