Common Lisp 有著威力強(qiáng)大的 I/O 工具。針對(duì)輸入以及一些普遍讀取字符的函數(shù),我們有?read
?,包含了一個(gè)完整的解析器 (parser)。針對(duì)輸出以及一些普遍寫(xiě)出字符的函數(shù),我們有?format
?,它自己幾乎就是一個(gè)語(yǔ)言。本章介紹了所有基本的概念。
Common Lisp 有兩種流 (streams),字符流與二進(jìn)制流。本章描述了字符流的操作;二進(jìn)制流的操作涵蓋在 14.2 節(jié)。
流是用來(lái)表示字符來(lái)源或終點(diǎn)的 Lisp 對(duì)象。要從文件讀取或?qū)懭?,你將文件作為流打開(kāi)。但流與文件是不一樣的。當(dāng)你在頂層讀入或印出時(shí),你也可以使用流。你甚至可以創(chuàng)建可以讀取或?qū)懭胱址牧鳌?/p>
輸入缺省是從?*standard-input*
?流讀取。輸出缺省是在?*standard-output*
?流。最初它們大概會(huì)在相同的地方:一個(gè)表示頂層的流。
我們已經(jīng)看過(guò)?read
?與?format
?是如何在頂層讀取與印出。前者接受一個(gè)應(yīng)是流的選擇性參數(shù),缺省是?*standard-input*
?。?format
的第一個(gè)參數(shù)也可以是一個(gè)流,但當(dāng)它是?t
?時(shí),輸出被送到?*standard-output*
?。所以我們目前為止都只用到缺省的流而已。我們可以在任何流上面做同樣的 I/O 操作。
路徑名(pathname)是一種指定一個(gè)文件的可移植方式。路徑名包含了六個(gè)部分:host、device、directory、name、type 及 version。你可以通過(guò)調(diào)用?make-pathname
?搭配一個(gè)或多個(gè)對(duì)應(yīng)的關(guān)鍵字參數(shù)來(lái)產(chǎn)生一個(gè)路徑。在最簡(jiǎn)單的情況下,你可以只指明名字,讓其他的部分留為缺省:
> (setf path (make-pathname :name "myfile"))
#P"myfile"
開(kāi)啟一個(gè)文件的基本函數(shù)是?open
?。它接受一個(gè)路徑名?[1]?以及大量的選擇性關(guān)鍵字參數(shù),而若是開(kāi)啟成功時(shí),返回一個(gè)指向文件的流。
你可以在創(chuàng)建流時(shí),指定你想要怎么使用它。 無(wú)論你是要寫(xiě)入流、從流讀取或者同時(shí)進(jìn)行讀寫(xiě)操作,都可以通過(guò)?direction
?參數(shù)設(shè)置。三個(gè)對(duì)應(yīng)的數(shù)值是?:input
?,?:output
?,?:io
?。如果是用來(lái)輸出的流,?if-exists
?參數(shù)說(shuō)明了如果文件已經(jīng)存在時(shí)該怎么做;通常它應(yīng)該是?:supersede
?(譯注: 取代)。所以要?jiǎng)?chuàng)建一個(gè)可以寫(xiě)至?"myfile"
?文件的流,你可以:
> (setf str (open path :direction :output
:if-exists :supersede))
#<Stream C017E6>
流的打印表示法因?qū)崿F(xiàn)而異。
現(xiàn)在我們可以把這個(gè)流作為第一個(gè)參數(shù)傳給?format
?,它會(huì)在流印出,而不是頂層:
> (format str "Something~%")
NIL
如果我們?cè)诖藭r(shí)檢查這個(gè)文件,可能有輸出,也可能沒(méi)有。某些實(shí)現(xiàn)會(huì)將輸出累積成一塊 (chunks)再輸出。直到我們將流關(guān)閉,它也許一直不會(huì)出現(xiàn):
> (close str)
NIL
當(dāng)你使用完時(shí),永遠(yuǎn)記得關(guān)閉文件;在你還沒(méi)關(guān)閉之前,內(nèi)容是不保證會(huì)出現(xiàn)的。現(xiàn)在如果我們檢查文件 “myfile” ,應(yīng)該有一行:
Something
如果我們只想從一個(gè)文件讀取,我們可以開(kāi)啟一個(gè)具有?:direction?:input
?的流 :
> (setf str (open path :direction :input))
#<Stream C01C86>
我們可以對(duì)一個(gè)文件使用任何輸入函數(shù)。7.2 節(jié)會(huì)更詳細(xì)的描述輸入。這里作為一個(gè)示例,我們將使用?read-line
?從文件來(lái)讀取一行文字:
> (read-line str)
"Something"
> (close str)
NIL
當(dāng)你讀取完畢時(shí),記得關(guān)閉文件。
大部分時(shí)間我們不使用?open
?與?close
?來(lái)操作文件的 I/O 。?with-open-file
?宏通常更方便。它的第一個(gè)參數(shù)應(yīng)該是一個(gè)列表,包含了變數(shù)名、伴隨著你想傳給?open
?的參數(shù)。在這之后,它接受一個(gè)代碼主體,它會(huì)被綁定至流的變數(shù)一起被求值,其中流是通過(guò)將剩余的參數(shù)傳給?open
?來(lái)創(chuàng)建的。之后這個(gè)流會(huì)被自動(dòng)關(guān)閉。所以整個(gè)文件寫(xiě)入動(dòng)作可以表示為:
(with-open-file (str path :direction :output
:if-exists :supersede)
(format str "Something~%"))
with-open-file
?宏將?close
?放在?unwind-protect
?里 (參見(jiàn) 92 頁(yè),譯注: 5.6 節(jié)),即使一個(gè)錯(cuò)誤打斷了主體的求值,文件是保證會(huì)被關(guān)閉的。
兩個(gè)最受歡迎的輸入函數(shù)是?read-line
?及?read
?。前者讀入換行符 (newline)之前的所有字符,并用字符串返回它們。它接受一個(gè)選擇性流參數(shù) (optional stream argument);若流忽略時(shí),缺省為?*standard-input*
?:
> (progn
(format t "Please enter your name: ")
(read-line))
Please enter your name: Rodrigo de Bivar
"Rodrigo de Bivar"
NIL
譯注:Rodrigo de Bivar 人稱(chēng)熙德 (El Cid),十一世紀(jì)的西班牙民族英雄。
如果你想要原封不動(dòng)的輸出,這是你該用的函數(shù)。(第二個(gè)返回值只在?read-line
?在遇到換行符之前,用盡輸入時(shí)返回真。)
在一般情況下,?read-line
?接受四個(gè)選擇性參數(shù): 一個(gè)流;一個(gè)參數(shù)用來(lái)決定遇到?end-of-file
?時(shí),是否產(chǎn)生錯(cuò)誤;若前一個(gè)參數(shù)為nil
?時(shí),該返回什么;第四個(gè)參數(shù) (在 235 頁(yè)討論)通常可以省略。
所以要在頂層顯示一個(gè)文件的內(nèi)容,我們可以使用下面這個(gè)函數(shù):
(defun pseudo-cat (file)
(with-open-file (str file :direction :input)
(do ((line (read-line str nil 'eof)
(read-line str nil 'eof)))
((eql line 'eof))
(format t "~A~%" line))))
如果我們想要把輸入解析為 Lisp 對(duì)象,使用?read
?。這個(gè)函數(shù)恰好讀取一個(gè)表達(dá)式,在表達(dá)式結(jié)束時(shí)停止讀取。所以可以讀取多于或少于一行。而當(dāng)然它所讀取的內(nèi)容必須是合法的 Lisp 語(yǔ)法。
如果我們?cè)陧攲邮褂?read
?,它會(huì)讓我們?cè)诒磉_(dá)式里面,想用幾個(gè)換行符就用幾個(gè):
> (read)
(a
b
c)
(A B C)
換句話說(shuō),如果我們?cè)谝恍欣锩孑斎朐S多表達(dá)式,?read
?會(huì)在第一個(gè)表達(dá)式之后,停止處理字符,留下剩余的字符給之后讀取這個(gè)流的函數(shù)處理。所以如果我們?cè)谝恍休斎攵鄠€(gè)表達(dá)式,來(lái)回應(yīng)?ask-number
?(20 頁(yè)。譯注:2.10 小節(jié))所印出提示符,會(huì)發(fā)生如下情形:
> (ask-number)
Please enter a number. a b
Please enter a number. Please enter a number. 43
43
兩個(gè)連續(xù)的提示符 (successive prompts)在第二行被印出。第一個(gè)?read
?調(diào)用會(huì)返回?a
?,而它不是一個(gè)數(shù)字,所以函數(shù)再次要求一個(gè)數(shù)字。但第一個(gè)?read
?只讀取到?a
?的結(jié)尾。所以下一個(gè)?read
?調(diào)用返回?b
?,導(dǎo)致了下一個(gè)提示符。
你或許想要避免使用?read
?來(lái)直接處理使用者的輸入。前述的函數(shù)若使用?read-line
?來(lái)獲得使用者輸入會(huì)比較好,然后對(duì)結(jié)果字符串調(diào)用?read-from-string
?。這個(gè)函數(shù)接受一個(gè)字符串,并返回第一個(gè)讀取的表達(dá)式:
> (read-from-string "a b c")
A
2
它同時(shí)返回第二個(gè)值,一個(gè)指出停止讀取字符串時(shí)的位置的數(shù)字。
在一般情況下,?read-from-string
?可以接受兩個(gè)選擇性參數(shù)與三個(gè)關(guān)鍵字參數(shù)。兩個(gè)選擇性參數(shù)是?read
?的第三、第四個(gè)參數(shù): 一個(gè) end-of-file (這個(gè)情況是字符串) 決定是否報(bào)錯(cuò),若不報(bào)錯(cuò)該返回什么。關(guān)鍵字參數(shù)?:start
?及?:end
?可以用來(lái)劃分從字符串的哪里開(kāi)始讀。
所有的這些輸入函數(shù)是由基本函數(shù) (primitive)?read-char
?所定義的,它讀取一個(gè)字符。它接受四個(gè)與?read
?及?read-line
?一樣的選擇性參數(shù)。Common Lisp 也定義一個(gè)函數(shù)叫做?peek-char
?,跟?read-char
?類(lèi)似,但不會(huì)將字符從流中移除。
三個(gè)最簡(jiǎn)單的輸出函數(shù)是?prin1
?,?princ
?以及?terpri
?。這三個(gè)函數(shù)的最后一個(gè)參數(shù)皆為選擇性的流參數(shù),缺省是?*standard-output*
?。
prin1
?與?princ
?的差別大致在于?prin1
?給程序產(chǎn)生輸出,而?princ
?給人類(lèi)產(chǎn)生輸出。所以舉例來(lái)說(shuō),?prin1
?會(huì)印出字符串左右的雙引號(hào),而?princ
?不會(huì):
> (prin1 "Hello")
"Hello"
"Hello"
> (princ "Hello")
Hello
"Hello"
兩者皆返回它們的第一個(gè)參數(shù) (譯注: 第二個(gè)值是返回值) ── 順道一提,是用?prin1
?印出。?terpri
?僅印出一新行。
有這些函數(shù)的背景知識(shí)在解釋更為通用的?format
?是很有用的。這個(gè)函數(shù)幾乎可以用在所有的輸出。他接受一個(gè)流 (或?t
?或?nil
?)、一個(gè)格式化字符串 (format string)以及零個(gè)或多個(gè)額外的參數(shù)。格式化字符串可以包含特定的格式化指令 (format directives),這些指令前面有波浪號(hào)?~
?。某些格式化指令作為字符串的占位符 (placeholder)使用。這些位置會(huì)被格式化字符串之后,所給入?yún)?shù)的表示法所取代。
如果我們把?t
?作為第一個(gè)參數(shù),輸出會(huì)被送至?*standard-output*
?。如果我們給?nil
?,?format
?會(huì)返回一個(gè)它會(huì)如何印出的字符串。為了保持簡(jiǎn)短,我們會(huì)在所有的示例里演示怎么做。
由于每人的觀點(diǎn)不同,?format
?可以是令人驚訝的強(qiáng)大或是極為可怕的復(fù)雜。有大量的格式化指令可用,而只有少部分會(huì)被大多數(shù)程序設(shè)計(jì)師使用。兩個(gè)最常用的格式化指令是?~A
?以及?~%
?。(你使用?~a
?或?~A
?都沒(méi)關(guān)系,但后者較常見(jiàn),因?yàn)樗尭袷交噶羁雌饋?lái)一目了然。) 一個(gè)?~A
?是一個(gè)值的占位符,它會(huì)像是用?princ
?印出一般。一個(gè)?~%
?代表著一個(gè)換行符 (newline)。
> (format nil "Dear ~A, ~% Our records indicate..."
"Mr. Malatesta")
"Dear Mr. Malatesta,
Our records indicate..."
這里?format
?返回了一個(gè)值,由一個(gè)含有換行符的字符串組成。
~S
?格式化指令像是?~A
?,但它使用?prin1
?印出對(duì)象,而不是?princ
?印出:
> (format t "~S ~A" "z" "z")
"z" z
NIL
格式化指令可以接受參數(shù)。?~F
?用來(lái)印出向右對(duì)齊 (right-justified)的浮點(diǎn)數(shù),可接受五個(gè)參數(shù):
下面是一個(gè)有五個(gè)參數(shù)的罕見(jiàn)例子:
? (format nil "~10,2,0,'*,' F" 26.21875)
" 26.22"
這是原本的數(shù)字取至小數(shù)點(diǎn)第二位、(小數(shù)點(diǎn)向左移 0 位)、在 10 個(gè)字符的空間里向右對(duì)齊,左邊補(bǔ)滿(mǎn)空白。注意作為參數(shù)給入是寫(xiě)成?'*
?而不是?#\*
?。由于數(shù)字塞得下 10 個(gè)字符,不需要使用第四個(gè)參數(shù)。
所有的這些參數(shù)都是選擇性的。要使用缺省值你可以直接忽略對(duì)應(yīng)的參數(shù)。如果我們想要做的是,印出一個(gè)小數(shù)點(diǎn)取至第二位的數(shù)字,我們可以說(shuō):
> (format nil "~,2,,,F" 26.21875)
"26.22"
你也可以忽略一系列的尾隨逗號(hào) (trailing commas),前面指令更常見(jiàn)的寫(xiě)法會(huì)是:
> (format nil "~,2F" 26.21875)
"26.22"
警告:?當(dāng)?format
?取整數(shù)時(shí),它不保證會(huì)向上進(jìn)位或向下舍入。就是說(shuō)?(format?nil?"~,1F"?1.25)
?可能會(huì)是?"1.2"
?或?"1.3"
?。所以如果你使用?format
?來(lái)顯示資訊時(shí),而使用者期望看到某種特定取整數(shù)方式的數(shù)字 (如: 金額數(shù)量),你應(yīng)該在印出之前先顯式地取好整數(shù)。
作為一個(gè) I/O 的示例,本節(jié)演示如何寫(xiě)一個(gè)簡(jiǎn)單的程序來(lái)對(duì)文本文件做字符串替換。我們即將寫(xiě)一個(gè)可以將一個(gè)文件中,舊的字符串old
?換成某個(gè)新的字符串?new
?的函數(shù)。最簡(jiǎn)單的實(shí)現(xiàn)方式是將輸入文件里的每一個(gè)字符與?old
?的第一個(gè)字符比較。如果沒(méi)有匹配,我們可以直接印出該字符至輸出。如果匹配了,我們可以將輸入的下一個(gè)字符與?old
?的第二個(gè)字符比較,等等。如果輸入字符與old
?完全相等時(shí),我們有一個(gè)成功的匹配,則我們印出?new
?至文件。
而要是?old
?在匹配途中失敗了,會(huì)發(fā)生什么事呢?舉例來(lái)說(shuō),假設(shè)我們要找的模式 (pattern)是?"abac"
?,而輸入文件包含的是"ababac"
?。輸入會(huì)一直到第四個(gè)字符才發(fā)現(xiàn)不匹配,也就是在模式中的?c
?以及輸入的?b
?才發(fā)現(xiàn)。在此時(shí)我們可以將原本的?a
?寫(xiě)至輸出文件,因?yàn)槲覀円呀?jīng)知道這里沒(méi)有匹配。但有些我們從輸入讀入的字符還是需要留著: 舉例來(lái)說(shuō),第三個(gè)?a
?,確實(shí)是成功匹配的開(kāi)始。所以在我們要實(shí)現(xiàn)這個(gè)算法之前,我們需要一個(gè)地方來(lái)儲(chǔ)存,我們已經(jīng)從輸入讀入的字符,但之后仍然需要的字符。
一個(gè)暫時(shí)儲(chǔ)存輸入的隊(duì)列 (queue)稱(chēng)作緩沖區(qū) (buffer)。在這個(gè)情況里,因?yàn)槲覀冎牢覀儾恍枰獌?chǔ)存超過(guò)一個(gè)預(yù)定的字符量,我們可以使用一個(gè)叫做環(huán)狀緩沖區(qū)?ring?buffer
?的資料結(jié)構(gòu)。一個(gè)環(huán)狀緩沖區(qū)實(shí)際上是一個(gè)向量。是使用的方式使其成為環(huán)狀: 我們將之后的元素所輸入進(jìn)來(lái)的值儲(chǔ)存起來(lái),而當(dāng)我們到達(dá)向量結(jié)尾時(shí),我們重頭開(kāi)始。如果我們不需要儲(chǔ)存超過(guò)?n
?個(gè)值,則我們只需要一個(gè)長(zhǎng)度為?n
?或是大于?n
?的向量,這樣我們就不需要覆寫(xiě)正在用的值。
在圖 7.1 的代碼,實(shí)現(xiàn)了環(huán)狀緩沖區(qū)的操作。?buf
?有五個(gè)字段 (field): 一個(gè)包含存入緩沖區(qū)的向量,四個(gè)其它字段用來(lái)放指向向量的索引 (indices)。兩個(gè)索引是?start
?與?end
?,任何環(huán)狀緩沖區(qū)的使用都會(huì)需要這兩個(gè)索引:?start
?指向緩沖區(qū)的第一個(gè)值,當(dāng)我們?nèi)〕鲆粋€(gè)值時(shí),?start
?會(huì)遞增 (incremented);?end
?指向緩沖區(qū)的最后一個(gè)值,當(dāng)我們插入一個(gè)新值時(shí),?end
?會(huì)遞增。
另外兩個(gè)索引,?used
?以及?new
?,是我們需要給這個(gè)應(yīng)用的基本環(huán)狀緩沖區(qū)所加入的東西。它們會(huì)介于?start
?與?end
?之間。實(shí)際上,它總是符合
start ≤ used ≤ new ≤ end
你可以把?used
?與?new
?想成是當(dāng)前匹配 (current match) 的?start
?與?end
?。當(dāng)我們開(kāi)始一輪匹配時(shí),?used
?會(huì)等于?start
?而?new
會(huì)等于?end
?。當(dāng)下一個(gè)字符 (successive character)匹配時(shí),我們需要遞增?used
?。當(dāng)?used
?與?new
?相等時(shí),我們將開(kāi)始匹配時(shí),所有存在緩沖區(qū)的字符讀入。我們不想要使用超過(guò)從匹配時(shí)所存在緩沖區(qū)的字符,或是重復(fù)使用同樣的字符。因此這個(gè)?new
?索引,開(kāi)始等于?end
?,但它不會(huì)在一輪匹配我們插入新字符至緩沖區(qū)一起遞增。
函數(shù)?bref
?接受一個(gè)緩沖區(qū)與一個(gè)索引,并返回索引所在位置的元素。借由使用?index
?對(duì)向量的長(zhǎng)度取?mod
?,我們可以假裝我們有一個(gè)任意長(zhǎng)的緩沖區(qū)。調(diào)用?(new-buf?n)
?會(huì)產(chǎn)生一個(gè)新的緩沖區(qū),能夠容納?n
?個(gè)對(duì)象。
要插入一個(gè)新值至緩沖區(qū),我們將使用?buf-insert
?。它將?end
?遞增,并把新的值放在那個(gè)位置 (譯注: 遞增完的位置)。相反的?buf-pop
?返回一個(gè)緩沖區(qū)的第一個(gè)數(shù)值,接著將?start
?遞增。任何環(huán)狀緩沖區(qū)都會(huì)有這兩個(gè)函數(shù)。
(defstruct buf
vec (start -1) (used -1) (new -1) (end -1))
(defun bref (buf n)
(svref (buf-vec buf)
(mod n (length (buf-vec buf)))))
(defun (setf bref) (val buf n)
(setf (svref (buf-vec buf)
(mod n (length (buf-vec buf))))
val))
(defun new-buf (len)
(make-buf :vec (make-array len)))
(defun buf-insert (x b)
(setf (bref b (incf (buf-end b))) x))
(defun buf-pop (b)
(prog1
(bref b (incf (buf-start b)))
(setf (buf-used b) (buf-start b)
(buf-new b) (buf-end b))))
(defun buf-next (b)
(when (< (buf-used b) (buf-new b))
(bref b (incf (buf-used b)))))
(defun buf-reset (b)
(setf (buf-used b) (buf-start b)
(buf-new b) (buf-end b)))
(defun buf-clear (b)
(setf (buf-start b) -1 (buf-used b) -1
(buf-new b) -1 (buf-end b) -1))
(defun buf-flush (b str)
(do ((i (1+ (buf-used b)) (1+ i)))
((> i (buf-end b)))
(princ (bref b i) str)))
圖 7.1 環(huán)狀緩沖區(qū)的操作
接下來(lái)我們需要兩個(gè)特別為這個(gè)應(yīng)用所寫(xiě)的函數(shù):?buf-next
?從緩沖區(qū)讀取一個(gè)值而不取出,而?buf-reset
?重置?used
?與?new
?到初始值,分別是?start
?與?end
?。如果我們已經(jīng)把至?new
?的值全部讀取完畢時(shí),?buf-next
?返回?nil
?。區(qū)別這個(gè)值與實(shí)際的值不會(huì)產(chǎn)生問(wèn)題,因?yàn)槲覀冎话阎荡嬖诰彌_區(qū)。
最后?buf-flush
?透過(guò)將所有作用的元素,寫(xiě)至由第二個(gè)參數(shù)所給入的流,而?buf-clear
?通過(guò)重置所有的索引至?-1
?將緩沖區(qū)清空。
在圖 7.1 定義的函數(shù)被圖 7.2 所使用,包含了字符串替換的代碼。函數(shù)?file-subst
?接受四個(gè)參數(shù);一個(gè)查詢(xún)字符串,一個(gè)替換字符串,一個(gè)輸入文件以及一個(gè)輸出文件。它創(chuàng)建了代表每個(gè)文件的流,然后調(diào)用?stream-subst
?來(lái)完成實(shí)際的工作。
第二個(gè)函數(shù)?stream-subst
?使用本節(jié)開(kāi)始所勾勒的算法。它一次從輸入流讀一個(gè)字符。直到輸入字符匹配要尋找的字符串時(shí),直接寫(xiě)至輸出流 (1)。當(dāng)一個(gè)匹配開(kāi)始時(shí),有關(guān)字符在緩沖區(qū)?buf
?排隊(duì)等候 (2)。
變數(shù)?pos
?指向我們想要匹配的字符在尋找字符串的所在位置。如果?pos
?等于這個(gè)字符串的長(zhǎng)度,我們有一個(gè)完整的匹配,則我們將替換字符串寫(xiě)至輸出流,并清空緩沖區(qū) (3)。如果在這之前匹配失敗,我們可以將緩沖區(qū)的第一個(gè)元素取出,并寫(xiě)至輸出流,之后我們重置緩沖區(qū),并從?pos
?等于 0 重新開(kāi)始 (4)。
(defun file-subst (old new file1 file2)
(with-open-file (in file1 :direction :input)
(with-open-file (out file2 :direction :output
:if-exists :supersede)
(stream-subst old new in out))))
(defun stream-subst (old new in out)
(let* ((pos 0)
(len (length old))
(buf (new-buf len))
(from-buf nil))
(do ((c (read-char in nil :eof)
(or (setf from-buf (buf-next buf))
(read-char in nil :eof))))
((eql c :eof))
(cond ((char= c (char old pos))
(incf pos)
(cond ((= pos len) ; 3
(princ new out)
(setf pos 0)
(buf-clear buf))
((not from-buf) ; 2
(buf-insert c buf))))
((zerop pos) ; 1
(princ c out)
(when from-buf
(buf-pop buf)
(buf-reset buf)))
(t ; 4
(unless from-buf
(buf-insert c buf))
(princ (buf-pop buf) out)
(buf-reset buf)
(setf pos 0))))
(buf-flush buf out)))
圖 7.2 字符串替換
下列表格展示了當(dāng)我們將文件中的?"baro"
?替換成?"baric"
?所發(fā)生的事,其中文件只有一個(gè)單字?"barbarous"
?:
CHARACTER | SOURCE | MATCH | CASE | OUTPUT | BUFFER |
---|---|---|---|---|---|
b | file | b | 2 | ? | b |
a | file | a | 2 | ? | b a |
r | file | r | 2 | ? | b a r |
b | file | o | 4 | b | b.a r b. |
a | buffer | b | 1 | a | a.r b. |
r | buffer | b | 1 | r | r.b. |
b | buffer | b | 1 | ? | r b: |
a | file | a | 2 | ? | r b:a |
r | file | r | 2 | ? | r b:a |
o | file | o | 3 | baric | r b:a r |
u | file | b | 1 | u | ? |
a | file | b | 1 | s | ? |
第一欄是當(dāng)前字符 ──?c
?的值;第二欄顯示是從緩沖區(qū)或是直接從輸入流讀取;第三欄顯示需要匹配的字符 ──?old
?的第?posth字符;第四欄顯示那一個(gè)條件式 (case)被求值作為結(jié)果;第五欄顯示被寫(xiě)至輸出流的字符;而最后一欄顯示緩沖區(qū)之后的內(nèi)容。在最后一欄里,?used
?與?new
?的位置一樣,由一個(gè)冒號(hào) (?:
?colon)表示。
在文件?"test1"
?里有如下文字:
The struggle between Liberty and Authority is the most conspicuous feature
in the portions of history with which we are earliest familiar, particularly
in that of Greece, Rome, and England.
在我們對(duì)?(file-subst?"?th"?"?z"?"test1"?"test2")
?求值之后,讀取文件?"test2"
?為:
The struggle between Liberty and Authority is ze most conspicuous feature
in ze portions of history with which we are earliest familiar, particularly
in zat of Greece, Rome, and England.
為了使這個(gè)例子盡可能的簡(jiǎn)單,圖 7.2 的代碼只將一個(gè)字符串換成另一個(gè)字符串。很容易擴(kuò)展為搜索一個(gè)模式而不是一個(gè)字面字符串。你只需要做的是,將?char=
?調(diào)用換成一個(gè)你想要的更通用的匹配函數(shù)調(diào)用。
一個(gè)宏字符 (macro character)是獲得?read
?特別待遇的字符。比如小寫(xiě)的?a
?,通常與小寫(xiě)?b
?一樣處理,但一個(gè)左括號(hào)就不同了: 它告訴 Lisp 開(kāi)始讀入一個(gè)列表。
一個(gè)宏字符或宏字符組合也稱(chēng)作?read-macro
?(讀取宏) 。許多 Common Lisp 預(yù)定義的讀取宏是縮寫(xiě)。比如說(shuō)引用 (Quote): 讀入一個(gè)像是?'a
?的表達(dá)式時(shí),它被讀取器展開(kāi)成?(quote?a)
?。當(dāng)你輸入引用的表達(dá)式 (quoted expression)至頂層時(shí),它們?cè)谧x入之時(shí)就會(huì)被求值,所以一般來(lái)說(shuō)你看不到這樣的轉(zhuǎn)換。你可以透過(guò)顯式調(diào)用?read
?使其現(xiàn)形:
> (car (read-from-string "'a"))
QUOTE
引用對(duì)于讀取宏來(lái)說(shuō)是不尋常的,因?yàn)樗脝我蛔址硎?。有了一個(gè)有限的字符集,你可以在 Common Lisp 里有許多單一字符的讀取宏,來(lái)表示一個(gè)或更多字符。
這樣的讀取宏叫做派發(fā) (dispatching)讀取宏,而第一個(gè)字符叫做派發(fā)字符 (dispatching character)。所有預(yù)定義的派發(fā)讀取宏使用井號(hào) (?#
?)作為派發(fā)字符。我們已經(jīng)見(jiàn)過(guò)好幾個(gè)。舉例來(lái)說(shuō),?#'
?是?(function?...)
?的縮寫(xiě),同樣的?'
?是?(quote?...)
?的縮寫(xiě)。
其它我們見(jiàn)過(guò)的派發(fā)讀取宏包括?#(...)
?,產(chǎn)生一個(gè)向量;?#nA(...)
?產(chǎn)生數(shù)組;?#\
?產(chǎn)生一個(gè)字符;?#S(n?...)
?產(chǎn)生一個(gè)結(jié)構(gòu)。當(dāng)這些類(lèi)型的每個(gè)對(duì)象被?prin1
?顯示時(shí) (或是?format
?搭配?~S
),它們使用對(duì)應(yīng)的讀取宏?[2]?。這表示著你可以寫(xiě)出或讀回這樣的對(duì)象:
> (let ((*print-array* t))
(vectorp (read-from-string (format nil "~S"
(vector 1 2)))))
T
當(dāng)然我們拿回來(lái)的不是同一個(gè)向量,而是具有同樣元素的新向量。
不是所有對(duì)象被顯示時(shí)都有著清楚 (distinct)、可讀的形式。舉例來(lái)說(shuō),函數(shù)與哈希表,傾向于這樣?#<...>
?被顯示。實(shí)際上?#<...>
也是一個(gè)讀取宏,但是特別用來(lái)產(chǎn)生當(dāng)遇到?read
?的錯(cuò)誤。函數(shù)與哈希表不能被寫(xiě)出與讀回來(lái),而這個(gè)讀取宏確保使用者不會(huì)有這樣的幻覺(jué)。?[3]
當(dāng)你定義你自己的事物表示法時(shí) (舉例來(lái)說(shuō),結(jié)構(gòu)的印出函數(shù)),你要將此準(zhǔn)則記住。要不使用一個(gè)可以被讀回來(lái)的表示法,或是使用#<...>
?。
format
?函數(shù)提供了完整的輸出控制。read
?遇到一個(gè)宏字符像是?'
?,它調(diào)用相關(guān)的函數(shù)。%
?字符表示。從這個(gè)字符開(kāi)始直到行尾都會(huì)被忽略。定義一個(gè)函數(shù),接受兩個(gè)文件名稱(chēng),并拷貝第一個(gè)文件的內(nèi)容去掉注解,寫(xiě)至第二個(gè)文件。array-dimensions
?(參見(jiàn) 361 頁(yè),譯注: Appendix D)。stream-subst
?來(lái)允許萬(wàn)用字符 (wildcard) 可以在模式中使用。若字符?+
?出現(xiàn)在?old
?里,它應(yīng)該匹配任何輸入字符。stream-subst
?來(lái)允許模式可以包含一個(gè)用來(lái)匹配任何數(shù)字的元素,以及一個(gè)可以匹配任何英文字符的元素或是一個(gè)可以匹配任何字符的元素。模式必須可以匹配任何特定的輸入字符。(提示:?old
?可以不是一個(gè)字符串。)腳注
[1] | 你可以給一個(gè)字符串取代路徑名,但這樣就不可攜了 (portable)。
[2] | 要讓向量與數(shù)組這樣被顯示,將?*print-array*
?設(shè)為真。
[3] | Lisp 不能只用?#'
?來(lái)表示函數(shù),因?yàn)?#'
?本身無(wú)法提供表示閉包的方式。
更多建議: