国产gaysexchina男同gay,japanrcep老熟妇乱子伦视频,吃奶呻吟打开双腿做受动态图,成人色网站,国产av一区二区三区最新精品

第十四章:進(jìn)階議題

2018-02-24 15:51 更新

本章是選擇性閱讀的。本章描述了 Common Lisp 里一些更深?yuàn)W的特性。Common Lisp 像是一個(gè)冰山:大部分的功能對(duì)于那些永遠(yuǎn)不需要他們的多數(shù)用戶是看不見(jiàn)的。你或許永遠(yuǎn)不需要自己定義包 (Package)或讀取宏 (read-macros),但當(dāng)你需要時(shí),有些例子可以讓你參考是很有用的。

14.1 類型標(biāo)識(shí)符 (Type Specifiers)

類型在 Common Lisp 里不是對(duì)象。舉例來(lái)說(shuō),沒(méi)有對(duì)象對(duì)應(yīng)到?integer?這個(gè)類型。我們像是從?type-of?函數(shù)里所獲得的,以及作為傳給像是?typep?函數(shù)的參數(shù),不是一個(gè)類型,而是一個(gè)類型標(biāo)識(shí)符 (type specifier)。

一個(gè)類型標(biāo)識(shí)符是一個(gè)類型的名稱。最簡(jiǎn)單的類型標(biāo)識(shí)符是像是?integer?的符號(hào)。這些符號(hào)形成了 Common Lisp 里的類型層級(jí)。在層級(jí)的最頂端是類型?t?── 所有的對(duì)象皆為類型?t?。而類型層級(jí)不是一棵樹(shù)。從?nil?至頂端有兩條路,舉例來(lái)說(shuō):一條從?atom,另一條從?list?與?sequence?。

一個(gè)類型實(shí)際上只是一個(gè)對(duì)象集合。這意味著有多少類型就有多少個(gè)對(duì)象的集合:一個(gè)無(wú)窮大的數(shù)目。我們可以用原子的類型標(biāo)識(shí)符 (atomic type specifiers)來(lái)表示某些集合:比如?integer?表示所有整數(shù)集合。但我們也可以建構(gòu)一個(gè)復(fù)合類型標(biāo)識(shí)符 (compound type specifiers)來(lái)參照到任何對(duì)象的集合。

舉例來(lái)說(shuō),如果?a?與?b?是兩個(gè)類型標(biāo)識(shí)符,則?(or?a?b)?表示分別由?a?與?b?類型所表示的聯(lián)集 (union)。也就是說(shuō),一個(gè)類型(or?a?b)?的對(duì)象是類型?a?或 類型?b?。

如果?circular??是一個(gè)對(duì)于?cdr?為環(huán)狀的列表返回真的函數(shù),則你可以使用適當(dāng)?shù)男蛄屑蟻?lái)表示:?[1]

(or vector (and list (not (satisfies circular?))))

某些原子的類型標(biāo)識(shí)符也可以出現(xiàn)在復(fù)合類型標(biāo)識(shí)符。要表示介于 1 至 100 的整數(shù)(包含),我們可以用:

(integer 1 100)

這樣的類型標(biāo)識(shí)符用來(lái)表示一個(gè)有限的類型 (finite type)。

在一個(gè)復(fù)合類型標(biāo)識(shí)符里,你可以通過(guò)在一個(gè)參數(shù)的位置使用?*?來(lái)留下某些未指定的信息。所以

(simple-array fixnum (* *))

描述了指定給?fixnum?使用的二維簡(jiǎn)單數(shù)組 (simple array)集合,而

(simple-array fixnum *)

描述了指定給?finxnum?使用的簡(jiǎn)單數(shù)組集合 (前者的超類型 「supertype」)。尾隨的星號(hào)可以省略,所以上個(gè)例子可以寫(xiě)為:

(simple-array fixnum)

若一個(gè)復(fù)合類型標(biāo)識(shí)符沒(méi)有傳入?yún)?shù),你可以使用一個(gè)原子。所以?simple-array?描述了所有簡(jiǎn)單數(shù)組的集合。

如果有某些復(fù)合類型標(biāo)識(shí)符你想重復(fù)使用,你可以使用?deftype?定義一個(gè)縮寫(xiě)。這個(gè)宏與?defmacro?相似,但會(huì)展開(kāi)成一個(gè)類型標(biāo)識(shí)符,而不是一個(gè)表達(dá)式。通過(guò)表達(dá)

(deftype proseq ()
        '(or vector (and list (not (satisfies circular?)))))

我們定義了?proseq?作為一個(gè)新的原子類型標(biāo)識(shí)符:

> (typep #(1 2) 'proseq)
T

如果你定義一個(gè)接受參數(shù)的類型標(biāo)識(shí)符,參數(shù)會(huì)被視為 Lisp 形式(即沒(méi)有被求值),與?defmacro?一樣。所以

(deftype multiple-of (n)
  `(and integer (satisfies (lambda (x)
                             (zerop (mod x ,n))))))

(譯注: 注意上面代碼是使用反引號(hào)?`````?)

定義了?(multiple-of n)?當(dāng)成所有?n?的倍數(shù)的標(biāo)識(shí)符:

> (type 12 '(multiple-of 4))
T

類型標(biāo)識(shí)符會(huì)被直譯 (interpreted),因此很慢,所以通常你最好定義一個(gè)函數(shù)來(lái)處理這類的測(cè)試。

14.2 二進(jìn)制流 (Binary Streams)

第 7 章曾提及的流有二進(jìn)制流 (binary streams)以及字符流 (character streams)。一個(gè)二進(jìn)制流是一個(gè)整數(shù)的來(lái)源及/或終點(diǎn),而不是字符。你通過(guò)指定一個(gè)整數(shù)的子類型來(lái)創(chuàng)建一個(gè)二進(jìn)制流 ── 當(dāng)你打開(kāi)流時(shí),通常是用?unsigned-byte?── 來(lái)作為?:element-type?的參數(shù)。

關(guān)于二進(jìn)制流的 I/O 函數(shù)僅有兩個(gè),?read-byte?以及?write-byte?。所以下面是如何定義復(fù)制一個(gè)文件的函數(shù):

(defun copy-file (from to)
  (with-open-file (in from :direction :input
                           :element-type 'unsigned-byte)
    (with-open-file (out to :direction :output
                            :element-type 'unsigned-byte)
      (do ((i (read-byte in nil -1)
              (read-byte in nil -1)))
          ((minusp i))
        (declare (fixnum i))
        (write-byte i out)))))

僅通過(guò)指定?unsigned-byte?給?:element-type?,你讓操作系統(tǒng)選擇一個(gè)字節(jié) (byte)的長(zhǎng)度。舉例來(lái)說(shuō),如果你明確地想要讀寫(xiě) 7 比特的整數(shù),你可以使用:

(unsigned-byte 7)

來(lái)傳給?:element-type?。

14.3 讀取宏 (Read-Macros)

7.5 節(jié)介紹過(guò)宏字符 (macro character)的概念,一個(gè)對(duì)于?read?有特別意義的字符。每一個(gè)這樣的字符,都有一個(gè)相關(guān)聯(lián)的函數(shù),這函數(shù)告訴?read?當(dāng)遇到這個(gè)字符時(shí)該怎么處理。你可以變更某個(gè)已存在宏字符所相關(guān)聯(lián)的函數(shù),或是自己定義新的宏字符。

函數(shù)?set-macro-character?提供了一種方式來(lái)定義讀取宏 (read-macros)。它接受一個(gè)字符及一個(gè)函數(shù),因此當(dāng)?read?碰到該字符時(shí),它返回調(diào)用傳入函數(shù)后的結(jié)果。

Lisp 中最古老的讀取宏之一是?'?,即?quote?。我們可以定義成:

(set-macro-character #\'
        #'(lambda (stream char)
                (list (quote quote) (read stream t nil t))))

當(dāng)?read?在一個(gè)普通的語(yǔ)境下遇到?'?時(shí),它會(huì)返回在當(dāng)前流和字符上調(diào)用這個(gè)函數(shù)的結(jié)果。(這個(gè)函數(shù)忽略了第二個(gè)參數(shù),第二個(gè)參數(shù)永遠(yuǎn)是引用字符。)所以當(dāng)?read?看到?'a?時(shí),會(huì)返回?(quote?a)?。

譯注:?read?函數(shù)接受的參數(shù)?(read?&optional?stream?eof-error?eof-value?recursive)

現(xiàn)在我們明白了?read?最后一個(gè)參數(shù)的用途。它表示無(wú)論?read?調(diào)用是否在另一個(gè)?read?里。傳給?read?的參數(shù)在幾乎所有的讀取宏里皆相同:傳入?yún)?shù)有流 (stream);接著是第二個(gè)參數(shù),?t?,說(shuō)明了?read?若讀入的東西是 end-of-file 時(shí),應(yīng)不應(yīng)該報(bào)錯(cuò);第三個(gè)參數(shù)說(shuō)明了不報(bào)錯(cuò)時(shí)要返回什么,因此在這里也就不重要了;而第四個(gè)參數(shù)?t?說(shuō)明了這個(gè)?read?調(diào)用是遞歸的。

(譯注:困惑的話可以看看?read 的定義?)

你可以(通過(guò)使用?make-dispatch-macro-character?)來(lái)定義你自己的派發(fā)宏字符(dispatching macro character),但由于?#已經(jīng)是一個(gè)宏字符,所以你也可以直接使用。六個(gè)?#?打頭的組合特別保留給你使用:?#!?、?#??、?##[?、?##]?、?#{?、?#}?。

你可以通過(guò)調(diào)用?set-dispatch-macro-character?定義新的派發(fā)宏字符組合,與?set-macro-character?類似,除了它接受兩個(gè)字符參數(shù)外。下面的代碼定義了?#??作為返回一個(gè)整數(shù)列表的讀取宏。

(set-dispatch-macro-character #\# #\?
  #'(lambda (stream char1 char2)
      (list 'quote
            (let ((lst nil))
              (dotimes (i (+ (read stream t nil t) 1))
                (push i lst))
              (nreverse lst)))))

現(xiàn)在?#?n?會(huì)被讀取成一個(gè)含有整數(shù)?0?至?n?的列表。舉例來(lái)說(shuō):

> #?7
(1 2 3 4 5 6 7)

除了簡(jiǎn)單的宏字符,最常定義的宏字符是列表分隔符 (list delimiters)。另一個(gè)保留給用戶的字符組是?#{?。以下我們定義了一種更復(fù)雜的左括號(hào):

(set-macro-character #\} (get-macro-character #\)))

(set-dispatch-macro-character #\# #\{
  #'(lambda (stream char1 char2)
      (let ((accum nil)
            (pair (read-delimited-list #\} stream t)))
        (do ((i (car pair) (+ i 1)))
            ((> i (cadr pair))
             (list 'quote (nreverse accum)))
          (push i accum)))))

這定義了一個(gè)這樣形式?#{x?y}?的表達(dá)式,使得這樣的表達(dá)式被讀取為所有介于?x?與?y?之間的整數(shù)列表,包含?x?與?y?:

> #{2 7}
(2 3 4 4 5 6 7)

函數(shù)?read-delimited-list?正是為了這樣的讀取宏而生的。它的第一個(gè)參數(shù)是被視為列表結(jié)束的字符。為了使?}?被識(shí)別為分隔符,必須先給它這個(gè)角色,所以程序在開(kāi)始的地方調(diào)用了?set-macro-character?。

如果你想要在定義一個(gè)讀取宏的文件里使用該讀取宏,則讀取宏的定義應(yīng)要包在一個(gè)?eval-when?表達(dá)式里,來(lái)確保它在編譯期會(huì)被求值。不然它的定義會(huì)被編譯,但不會(huì)被求值,直到編譯文件被載入時(shí)才會(huì)被求值。

14.4 包 (Packages)

一個(gè)包是一個(gè)將名字映對(duì)到符號(hào)的 Lisp 對(duì)象。當(dāng)前的包總是存在全局變量?*package*?里。當(dāng) Common Lisp 啟動(dòng)時(shí),當(dāng)前的包會(huì)是*common-lisp-user*?,通常稱為用戶包 (user package)。函數(shù)?package-name?返回包的名字,而?find-package?返回一個(gè)給定名稱的包:

> (package-name *package*)
"COMMON-LISP-USER"
> (find-package "COMMON-LISP-USER")
#<Package "COMMON-LISP-USER" 4CD15E>

通常一個(gè)符號(hào)在讀入時(shí)就被 interned 至當(dāng)前的包里面了。函數(shù)?symbol-package?接受一個(gè)符號(hào)并返回該符號(hào)被 interned 的包。

(symbol-package 'sym)
#<Package "COMMON-LISP-USER" 4CD15E>

有趣的是,這個(gè)表達(dá)式返回它該返回的值,因?yàn)楸磉_(dá)式在可以被求值前必須先被讀入,而讀取這個(gè)表達(dá)式導(dǎo)致?sym?被 interned。為了之后的用途,讓我們給?sym?一個(gè)值:

> (setf sym 99)
99

現(xiàn)在我們可以創(chuàng)建及切換至一個(gè)新的包:

> (setf *package* (make-package 'mine
                                :use '(common-lisp)))
#<Package "MINE" 63390E>

現(xiàn)在應(yīng)該會(huì)聽(tīng)到詭異的背景音樂(lè),因?yàn)槲覀儊?lái)到一個(gè)不一樣的世界了: 在這里?sym?不再是本來(lái)的?sym?了。

MINE> sym
Error: SYM has no value

為什么會(huì)這樣?因?yàn)樯厦嫖覀冊(cè)O(shè)為 99 的?sym?與?mine?里的?sym?是兩個(gè)不同的符號(hào)。?[2]?要在用戶包之外參照到原來(lái)的?sym?,我們必須把包的名字加上兩個(gè)冒號(hào)作為前綴:

MINE> common-lisp-user::sym
99

所以有著相同打印名稱的不同符號(hào)能夠在不同的包內(nèi)共存??梢杂幸粋€(gè)?sym?在?common-lisp-user?包,而另一個(gè)?sym?在?mine?包,而他們會(huì)是不一樣的符號(hào)。這就是包存在的意義。如果你在分開(kāi)的包內(nèi)寫(xiě)你的程序,你大可放心選擇函數(shù)與變量的名字,而不用擔(dān)心某人使用了同樣的名字。即便是他們使用了同樣的名字,也不會(huì)是相同的符號(hào)。

包也提供了信息隱藏的手段。程序應(yīng)通過(guò)函數(shù)與變量的名字來(lái)參照它們。如果你不讓一個(gè)名字在你的包之外可見(jiàn)的話,那么另一個(gè)包中的代碼就無(wú)法使用或者修改這個(gè)名字所參照的對(duì)象。

通常使用兩個(gè)冒號(hào)作為包的前綴也是很差的風(fēng)格。這么做你就違反了包本應(yīng)提供的模塊性。如果你不得不使用一個(gè)雙冒號(hào)來(lái)參照到一個(gè)符號(hào),這是因?yàn)槟橙烁静幌胱屇阌谩?/p>

通常我們應(yīng)該只參照被輸出 (?exported?)的符號(hào)。如果我們回到用戶包里,并輸出一個(gè)被 interned 的符號(hào),

MINE> (in-package common-lisp-user)
#<Package "COMMON-LISP-USER" 4CD15E>
> (export 'bar)
T
> (setf bar 5)
5

我們使這個(gè)符號(hào)對(duì)于其它的包是可視的?,F(xiàn)在當(dāng)我們回到?mine?,我們可以僅使用單冒號(hào)來(lái)參照到?bar?,因?yàn)樗且粋€(gè)公開(kāi)可用的名字:

> (in-package mine)
#<Package "MINE" 63390E>
MINE> common-lisp-user:bar
5

通過(guò)把?bar?輸入 (?import?)至?mine?包,我們就能進(jìn)一步讓?mine?和?user?包可以共享?bar?這個(gè)符號(hào):

MINE> (import 'common-lisp-user:bar)
T
MINE> bar
5

在輸入?bar?之后,我們根本不需要用任何包的限定符 (package qualifier),就能參照它了。這兩個(gè)包現(xiàn)在共享了同樣的符號(hào);不可能會(huì)有一個(gè)獨(dú)立的?mine:bar?了。

要是已經(jīng)有一個(gè)了怎么辦?在這種情況下,?import?調(diào)用會(huì)產(chǎn)生一個(gè)錯(cuò)誤,如下面我們?cè)囍斎?sym?時(shí)便知:

MINE> (import 'common-lisp-user::sym)
Error: SYM is already present in MINE.

在此之前,當(dāng)我們?cè)囍?mine?包里對(duì)?sym?進(jìn)行了一次不成功的求值,我們使?sym?被 interned 至?mine?包里。而因?yàn)樗鼪](méi)有值,所以產(chǎn)生了一個(gè)錯(cuò)誤,但輸入符號(hào)名的后果就是使這個(gè)符號(hào)被 intern 進(jìn)這個(gè)包。所以現(xiàn)在當(dāng)我們?cè)囍斎?sym?至?mine?包里,已經(jīng)有一個(gè)相同名稱的符號(hào)了。

另一個(gè)方法來(lái)獲得別的包內(nèi)符號(hào)的存取權(quán)是使用(?use?)它:

MINE> (use-package 'common-lisp-user)
T

現(xiàn)在所有由用戶包 (譯注: common-lisp-user 包)所輸出的符號(hào),可以不需要使用任何限定符在?mine?包里使用。(如果?sym?已經(jīng)被用戶包輸出了,這個(gè)調(diào)用也會(huì)產(chǎn)生一個(gè)錯(cuò)誤。)

含有自帶操作符及變量名字的包叫做?common-lisp?。由于我們將這個(gè)包的名字在創(chuàng)建?mine?包時(shí)作為?make-package?的?:use?參數(shù),所有的 Common Lisp 自帶的名字在?mine?里都是可視的:

MINE> #'cons
#<Compiled-Function CONS 462A3E>

在編譯后的代碼中, 通常不會(huì)像這樣在頂層進(jìn)行包的操作。更常見(jiàn)的是包的調(diào)用會(huì)包含在源文件里。通常,只要把?in-package?和defpackage?放在源文件的開(kāi)頭就可以了,正如 137 頁(yè)所示。

這種由包所提供的模塊性實(shí)際上有點(diǎn)奇怪。我們不是對(duì)象的模塊 (modules),而是名字的模塊。

每一個(gè)使用了?common-lisp?的包,都可以存取?cons?,因?yàn)?common-lisp?包里有一個(gè)叫這個(gè)名字的函數(shù)。但這會(huì)導(dǎo)致一個(gè)名字為cons?的變量也會(huì)在每個(gè)使用了?common-lisp?包里是可視的。如果包使你困惑,這就是主要的原因;因?yàn)榘皇腔趯?duì)象而是基于名字。

14.5 Loop 宏 (The Loop Facility)

loop?宏最初是設(shè)計(jì)來(lái)幫助無(wú)經(jīng)驗(yàn)的 Lisp 用戶來(lái)寫(xiě)出迭代的代碼。與其撰寫(xiě) Lisp 代碼,你用一種更接近英語(yǔ)的形式來(lái)表達(dá)你的程序,然后這個(gè)形式被翻譯成 Lisp。不幸的是,?loop?比原先設(shè)計(jì)者預(yù)期的更接近英語(yǔ):你可以在簡(jiǎn)單的情況下使用它,而不需了解它是如何工作的,但想在抽象層面上理解它幾乎是不可能的。

如果你是曾經(jīng)計(jì)劃某天要理解?loop?怎么工作的許多 Lisp 程序員之一,有一些好消息與壞消息。好消息是你并不孤單:幾乎沒(méi)有人理解它。壞消息是你永遠(yuǎn)不會(huì)理解它,因?yàn)?ANSI 標(biāo)準(zhǔn)實(shí)際上并沒(méi)有給出它行為的正式規(guī)范。

這個(gè)宏唯一的實(shí)際定義是它的實(shí)現(xiàn)方式,而唯一可以理解它(如果有人可以理解的話)的方法是通過(guò)實(shí)例。ANSI 標(biāo)準(zhǔn)討論?loop?的章節(jié)大部分由例子組成,而我們將會(huì)使用同樣的方式來(lái)介紹相關(guān)的基礎(chǔ)概念。

第一個(gè)關(guān)于?loop?宏我們要注意到的是語(yǔ)法 (?syntax?)。一個(gè)?loop?表達(dá)式不是包含子表達(dá)式而是子句 (clauses)。這些子句不是由括號(hào)分隔出來(lái);而是每種都有一個(gè)不同的語(yǔ)法。在這個(gè)方面上,?loop?與傳統(tǒng)的 Algol-like 語(yǔ)言相似。但其它?loop?獨(dú)特的特性,使得它與 Algol 不同,也就是在?loop?宏里調(diào)換子句的順序與會(huì)發(fā)生的事情沒(méi)有太大的關(guān)聯(lián)。

一個(gè)?loop?表達(dá)式的求值分為三個(gè)階段,而一個(gè)給定的子句可以替多于一個(gè)的階段貢獻(xiàn)代碼。這些階段如下:

  1. 序幕?(Prologue)。 被求值一次來(lái)做為迭代過(guò)程的序幕。包括了將變量設(shè)至它們的初始值。
  2. 主體?(Body) 每一次迭代時(shí)都會(huì)被求值。
  3. 閉幕?(Epilogue) 當(dāng)?shù)Y(jié)束時(shí)被求值。決定了?loop?表達(dá)式的返回值(可能返回多個(gè)值)。

我們會(huì)看幾個(gè)?loop?子句的例子,并考慮何種代碼會(huì)貢獻(xiàn)至何個(gè)階段。

舉例來(lái)說(shuō),最簡(jiǎn)單的?loop?表達(dá)式,我們可能會(huì)看到像是下列的代碼:

> (loop for x from 0 to 9
        do (princ x))
0123456789
NIL

這個(gè)?loop?表達(dá)式印出從?0?至?9?的整數(shù),并返回?nil?。第一個(gè)子句,

for?x?from?0?to?9

貢獻(xiàn)代碼至前兩個(gè)階段,導(dǎo)致?x?在序幕中被設(shè)為?0?,在主體開(kāi)頭與?9?來(lái)做比較,在主體結(jié)尾被遞增。第二個(gè)子句,

do?(princ?x)

貢獻(xiàn)代碼給主體。

一個(gè)更通用的?for?子句說(shuō)明了起始與更新的形式 (initial and update form)。停止迭代可以被像是?while?或?until?子句來(lái)控制。

> (loop for x = 8 then (/ x 2)
        until (< x 1)
        do (princ x))
8421
NIL

你可以使用?and?來(lái)創(chuàng)建復(fù)合的?for?子句,同時(shí)初始及更新兩個(gè)變量:

> (loop for x from 1 to 4
        and y from 1 to 4
        do (princ (list x y)))
(1 1)(2 2)(3 3)(4 4)
NIL

要不然有多重?for?子句時(shí),變量會(huì)被循序更新。

另一件在迭代代碼通常會(huì)做的事是累積某種值。舉例來(lái)說(shuō):

> (loop for x in '(1 2 3 4)
        collect (1+ x))
(2 3 4 5)

在?for?子句使用?in?而不是?from?,導(dǎo)致變量被設(shè)為一個(gè)列表的后續(xù)元素,而不是連續(xù)的整數(shù)。

在這個(gè)情況里,?collect?子句貢獻(xiàn)代碼至三個(gè)階段。在序幕,一個(gè)匿名累加器 (anonymous accumulator)設(shè)為?nil?;在主體裡,(1+?x)?被累加至這個(gè)累加器,而在閉幕時(shí)返回累加器的值。

這是返回一個(gè)特定值的第一個(gè)例子。有用來(lái)明確指定返回值的子句,但沒(méi)有這些子句時(shí),一個(gè)?collect?子句決定了返回值。所以我們?cè)谶@里所做的其實(shí)是重復(fù)了?mapcar?。

loop?最常見(jiàn)的用途大概是蒐集調(diào)用一個(gè)函數(shù)數(shù)次的結(jié)果:

> (loop for x from 1 to 5
        collect (random 10))
(3 8 6 5 0)

這里我們獲得了一個(gè)含五個(gè)隨機(jī)數(shù)的列表。這跟我們定義過(guò)的?map-int?情況類似 (105 頁(yè)「譯注: 6.4 小節(jié)?!?。如果我們有了?loop,為什么還需要?map-int??另一個(gè)人也可以說(shuō),如果我們有了?map-int?,為什么還需要?loop??

一個(gè)?collect?子句也可以累積值到一個(gè)有名字的變量上。下面的函數(shù)接受一個(gè)數(shù)字的列表并返回偶數(shù)與奇數(shù)列表:

(defun even/odd (ns)
  (loop for n in ns
        if (evenp n)
           collect n into evens
           else collect n into odds
        finally (return (values evens odds))))

一個(gè)?finally?子句貢獻(xiàn)代碼至閉幕。在這個(gè)情況它指定了返回值。

一個(gè)?sum?子句和一個(gè)?collect?子句類似,但?sum?子句累積一個(gè)數(shù)字,而不是一個(gè)列表。要獲得?1?至?n?的和,我們可以寫(xiě):

(defun sum (n)
  (loop for x from 1 to n
        sum x))

loop?更進(jìn)一步的細(xì)節(jié)在附錄 D 討論,從 325 頁(yè)開(kāi)始。舉個(gè)例子,圖 14.1 包含了先前章節(jié)的兩個(gè)迭代函數(shù),而圖 14.2 演示了將同樣的函數(shù)翻譯成?loop?。

(defun most (fn lst)
  (if (null lst)
      (values nil nil)
      (let* ((wins (car lst))
             (max (funcall fn wins)))
        (dolist (obj (cdr lst))
          (let ((score (funcall fn obj)))
            (when (> score max)
              (setf wins obj
                    max  score))))
        (values wins max))))

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

圖 14.1 不使用 loop 的迭代函數(shù)

(defun most (fn lst)
  (if (null lst)
      (values nil nil)
      (loop with wins = (car lst)
            with max = (funcall fn wins)
            for obj in (cdr lst)
            for score = (funcall fn obj)
            when (> score max)
                 (do (setf wins obj
                           max score)
            finally (return (values wins max))))))

(defun num-year (n)
  (if (< n 0)
      (loop for y downfrom (- yzero 1)
            until (<= d n)
            sum (- (year-days y)) into d
            finally (return (values (+ y 1) (- n d))))
      (loop with prev = 0
            for y from yzero
            until (> d n)
            do (setf prev d)
            sum (year-days y) into d
            finally (return (values (- y 1)
                                    (- n prev))))))

圖 14.2 使用 loop 的迭代函數(shù)

一個(gè)?loop?的子句可以參照到由另一個(gè)子句所設(shè)置的變量。舉例來(lái)說(shuō),在?even/odd?的定義里面,?finally?子句參照到由兩個(gè)collect?子句所創(chuàng)建的變量。這些變量之間的關(guān)系,是?loop?定義最含糊不清的地方??紤]下列兩個(gè)表達(dá)式:

(loop for y = 0 then z
      for x from 1 to 5
      sum 1 into z
      finally (return y z))

(loop for x from 1 to 5
      for y = 0 then z
      sum 1 into z
      finally (return y z))

它們看起來(lái)夠簡(jiǎn)單 ── 每一個(gè)有四個(gè)子句。但它們返回同樣的值嗎?它們返回的值多少?你若試著在標(biāo)準(zhǔn)中想找答案將徒勞無(wú)功。每一個(gè)?loop?子句本身是夠簡(jiǎn)單的。但它們組合起來(lái)的方式是極為復(fù)雜的 ── 而最終,甚至標(biāo)準(zhǔn)里也沒(méi)有明確定義。

由于這類原因,使用?loop?是不推薦的。推薦?loop?的理由,你最多可以說(shuō),在像是圖 14.2 這般經(jīng)典的例子中,?loop?讓代碼看起來(lái)更容易理解。

14.6 狀況 (Conditions)

在 Common Lisp 里,狀況 (condition)包括了錯(cuò)誤以及其它可能在執(zhí)行期發(fā)生的情況。當(dāng)一個(gè)狀況被捕捉時(shí) (signalled),相應(yīng)的處理程序 (handler)會(huì)被調(diào)用。處理錯(cuò)誤狀況的缺省處理程序通常會(huì)調(diào)用一個(gè)中斷循環(huán) (break-loop)。但 Common Lisp 提供了多樣的操作符來(lái)捕捉及處理錯(cuò)誤。要覆寫(xiě)缺省的處理程序,甚至是自己寫(xiě)一個(gè)新的處理程序也是有可能的。

多數(shù)的程序員不會(huì)直接處理狀況。然而有許多更抽象的操作符使用了狀況,而要了解這些操作符,知道背后的原理是很有用的。

Common lisp 有數(shù)個(gè)操作符用來(lái)捕捉錯(cuò)誤。最基本的是?error?。一個(gè)調(diào)用它的方法是給入你會(huì)給?format?的相同參數(shù):

> (error "Your report uses ~A as a verb." 'status)
Error: Your report uses STATUS as a verb
                         Options: :abort, :backtrace
>>

如上所示,除非這樣的狀況被處理好了,不然執(zhí)行就會(huì)被打斷。

用來(lái)捕捉錯(cuò)誤的更抽象操作符包括了?ecase?、?check-type?以及?assert?。前者與?case?相似,要是沒(méi)有鍵值匹配時(shí)會(huì)捕捉一個(gè)錯(cuò)誤:

> (ecase 1 (2 3) (4 5))
Error: No applicable clause
                         Options: :abort, :backtrace
>>

普通的?case?在沒(méi)有鍵值匹配時(shí)會(huì)返回?nil?,但由于利用這個(gè)返回值是很差的編碼風(fēng)格,你或許會(huì)在當(dāng)你沒(méi)有?otherwise?子句時(shí)使用?ecase?。

check-type?宏接受一個(gè)位置,一個(gè)類型名以及一個(gè)選擇性字符串,并在該位置的值不是預(yù)期的類型時(shí),捕捉一個(gè)可修正的錯(cuò)誤 (correctable error)。一個(gè)可修正錯(cuò)誤的處理程序會(huì)給我們一個(gè)機(jī)會(huì)來(lái)提供一個(gè)新的值:

> (let ((x '(a b c)))
                (check-type (car x) integer "an integer")
                x)
Error: The value of (CAR X), A, should be an integer.
Options: :abort, :backtrace, :continue
>> :continue
New value of (CAR X)? 99
(99 B C)
>

在這個(gè)例子里,?(car?x)?被設(shè)為我們提供的新值,并重新執(zhí)行,返回了要是?(car?x)?本來(lái)就包含我們所提供的值所會(huì)返回的結(jié)果。

這個(gè)宏是用更通用的?assert?所定義的,?assert?接受一個(gè)測(cè)試表達(dá)式以及一個(gè)有著一個(gè)或多個(gè)位置的列表,伴隨著你可能傳給error?的參數(shù):

> (let ((sandwich '(ham on rye)))
    (assert (eql (car sandwich) 'chicken)
            ((car sandwich))
            "I wanted a ~A sandwich." 'chicken)
    sandwich)
Error: I wanted a CHICKEN sandwich.
Options: :abort, :backtrace, :continue
>> :continue
New value of (CAR SANDWICH)? 'chicken
(CHICKEN ON RYE)

要建立新的處理程序也是可能的,但大多數(shù)程序員只會(huì)間接的利用這個(gè)可能性,通過(guò)使用像是?ignore-errors?的宏。如果它的參數(shù)沒(méi)產(chǎn)生錯(cuò)誤時(shí)像在?progn?里求值一樣,但要是在求值過(guò)程中,不管什么參數(shù)報(bào)錯(cuò),執(zhí)行是不會(huì)被打斷的。取而代之的是,?ignore-errors?表達(dá)式會(huì)直接返回兩個(gè)值:?nil?以及捕捉到的狀況。

舉例來(lái)說(shuō),如果在某個(gè)時(shí)候,你想要用戶能夠輸入一個(gè)表達(dá)式,但你不想要在輸入是語(yǔ)法上不合時(shí)中斷執(zhí)行,你可以這樣寫(xiě):

(defun user-input (prompt)
  (format t prompt)
  (let ((str (read-line)))
    (or (ignore-errors (read-from-string str))
        nil)))

若輸入包含語(yǔ)法錯(cuò)誤時(shí),這個(gè)函數(shù)僅返回?nil?:

> (user-input "Please type an expression")
Please type an expression> #%@#+!!
NIL

腳注

[1] | 雖然標(biāo)準(zhǔn)沒(méi)有提到這件事,你可以假定?and?以及?or?類型標(biāo)示符僅考慮它們所要考慮的參數(shù),與?or?及?and?宏類似。

[2] | 某些 Common Lisp 實(shí)現(xiàn),當(dāng)我們不在用戶包下時(shí),會(huì)在頂層提示符前打印包的名字。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)