第6章 構(gòu)建軟件工具
所謂人,就是能夠使用工具的動(dòng)物。沒有工具就無從著力,有了工具則所向披靡。
——托馬斯?卡萊爾(1795-1881)
在第4章和第5章,我們主要是構(gòu)建了兩個(gè)特定程序,GPS和ELIZA。在本章,我們來復(fù)習(xí)一下這兩個(gè)程序,來找找看一些普遍的模式。這些從可重用軟件工具中抽象出來模式將會(huì)在后續(xù)章節(jié)中起到一些幫助作用。
6.1 一個(gè)交互式的解釋器工具
這個(gè)函數(shù)結(jié)構(gòu)是eliza的一個(gè)通用版本,這里再提點(diǎn)一下:
(defun eliza ()
?“Respond to user input using pattern matching rules.”
?(loop
? ?(print ‘eliza>)
? ?(print (flatten (use-eliza-rules (read))))))
其他很多應(yīng)用都是用了這種模式,包括Lisp本身。Lisp的頂層表現(xiàn)可以定義成這樣子:
(defun lisp ()
?(loop
? ?(print ‘>)
? ?(print (eval (read)))))
一個(gè)Lisp系統(tǒng)的頂層表現(xiàn)歷史上一般就稱作“讀取-求值-打印 循環(huán)”這樣的過程。大部分Lisp會(huì)在讀取輸入之前打印一個(gè)提示符,所以實(shí)際上應(yīng)該是“提示符-讀取-求值-打印 循環(huán)”才對(duì),但是在一些早期的系統(tǒng)中,比如MacLisp就是沒有提示符的。如果我們不考慮提示符的話,也可以僅用四個(gè)符號(hào)就寫出一個(gè)完整的Lisp解釋器:
(loop (print (eval (read))))
僅僅用這四個(gè)符號(hào)和八個(gè)括號(hào)就像構(gòu)建一個(gè)Lisp解釋器,聽上去是像是在開玩笑。那這一行代碼,到底是寫出了什么意思呢?這個(gè)問題的一個(gè)答案就是去想象,如果用Pascal語言來寫一個(gè)Lisp或者Pascal的解釋器,我們究竟要做什么。需要一個(gè)詞法分析器和一個(gè)符號(hào)表管理器。這些都是在工作的范圍內(nèi),但是read可以處理這些東西。還需要語法分析器來聚合詞法分隔符形成語句。Read也把這活兒干了,但只是因?yàn)長(zhǎng)isp的語句的語法是任意的,也就是列表和原子的語法。因此read在Lisp中扮演了一個(gè)很好的語法分析器的角色,但是在Pascal中是不行的。接下來,就是解釋器的求值或者解釋部分;eval完成了這項(xiàng)功能,并且也可以像處理Lisp表達(dá)式那樣處理好Pascal的語句。Print所做的要比read和eval少得多,但是仍然是很有必要的。
重點(diǎn)并不是在于一行代碼就可以被看做是一個(gè)Lisp的實(shí)現(xiàn),而是要看做是計(jì)算過程的一般模式。ELIZA和Lisp都可以看做是交互式的解釋器,讀取一個(gè)輸入,以某種方式轉(zhuǎn)化或者求值輸入,打印結(jié)果,之后返回等待更多的輸入。我們從中可以提取出如下的一般模式:
(defun program ()
?(loop
? ? (print prompt)
? ? (print (transform (read)))))
有兩種方式來利用這些遞歸模式:正規(guī)路子和野路子。先說野路子,將模式看做一個(gè)模板或是一種泛型,在程序設(shè)計(jì)過程中根據(jù)應(yīng)用的不同屢次使用。當(dāng)我們要寫一個(gè)新程序,我們回想起寫過的或者看到過的相似的程序,回頭看看那個(gè)程序,吧相關(guān)的部分留下,之后修改為新程序做些修改就可以了。如果借用的程序部分比較多的話,在新程序中用注釋標(biāo)記一下原始程序的部分是一個(gè)比較好的做法,但是在原始程序和導(dǎo)出程序之間,是沒有什么“官方”的連接的。
正規(guī)路子就是創(chuàng)建一種抽象,以函數(shù)的形式或者以數(shù)據(jù)結(jié)構(gòu)的形式,顯式地指向每一個(gè)新的應(yīng)用——就是說,以一個(gè)可用軟件工具的形式來適應(yīng)抽象。解釋器模式可以被抽象成一個(gè)如下的函數(shù):
(defun interactive-interpreter (prompt transformer)
?“Read an expression, transform it, and print the result.”
? (loop
? ? (print pronmpt)
? ? (print (funcall transformer (read)))))
這個(gè)函數(shù)可以用來寫每一個(gè)新的解釋器:
(defun lisp ()
(interactive-interpreter ‘> #’eval))
(defun eliza ()
(interactive-interpreter ‘eliza>
#’(lambda (x) (flatten (use-eliza-rules x)))))
或者,可以借用高階函數(shù)compose:
(defun compose (f g)
“Return the function that computes (f (g x)).”
#’(lambda (x) (funcall f (funcall g x))))
(defun eliza ()
(interactive-interpreter ‘eliza>
(compose #’flatten #’use-eliza-rules)))
在正規(guī)路子和野路子之間有兩個(gè)主要的區(qū)別。首先,他們看上去不一樣。如果是一個(gè)簡(jiǎn)單的抽象,就像上面那個(gè),讀取一個(gè)有顯式循環(huán)輸入發(fā)熱表達(dá)式要比讀取一個(gè)調(diào)用interactive-interpreter的表達(dá)式簡(jiǎn)單得多,因?yàn)楹笳咝枰业絠nteractive-interpreter的定義,還要理解定義才可以。
另一個(gè)區(qū)別在維護(hù)性上體現(xiàn)出來。假設(shè)我們?cè)诮换ナ浇忉屍鞯亩x中遺漏了一個(gè)特性。比如說疏忽了Loop的出口。我們就需要假定,用戶可以用一些中斷信息按鍵來結(jié)束循環(huán)。一個(gè)比較干凈的實(shí)現(xiàn)是允許用戶給解釋器一個(gè)顯式的結(jié)束命令。另一個(gè)有用的特性就是在解釋器內(nèi)除處理錯(cuò)誤。如果我們使用野路子,給程序添加一個(gè)這樣的特性就不會(huì)影響其他程序了。但是如果我們使用正規(guī)路子,之后對(duì)interactive-interpreter的所有改動(dòng)將會(huì)自動(dòng)給所有使用它的程序帶來新的特性。
后面的interactive-interpreter版本增加了兩個(gè)新的特性。首先,他使用宏handler-case來處理錯(cuò)誤。這個(gè)宏會(huì)先求值第一個(gè)參數(shù),然后返回第一個(gè)參數(shù)的值。但是如果有錯(cuò)誤發(fā)生的話,后面的參數(shù)就會(huì)根據(jù)已發(fā)生的錯(cuò)誤進(jìn)行錯(cuò)誤條件檢查。這么用的話,error會(huì)匹配所有的錯(cuò)誤,才去的行動(dòng)就是打印錯(cuò)誤條件之后繼續(xù)。
這個(gè)版本也允許提示字符串或者一個(gè)沒有參數(shù)的函數(shù),函數(shù)會(huì)被調(diào)用打印提舒服。函數(shù)prompt-generator,會(huì)返回一個(gè)函數(shù)來打印形式1,2等等的提示符。
(defun interactive-interpreter (prompt transformer)
?“Read an expression, transform it, and print the result.”
?(loop
? ?(handler-case
? ? ?(progn
? ? ? ?(if (string prompt)
? ? ? ? ?(print prompt)
? ? ? ? ?(funcall prompt))
? ? ? ?(print (funcall transformer (read))))
? ? ?;; In case of error, do this:
? ? ?(error (condition)
? ? ? ?(format t “~&;; Error ~a ignored, back to top level.”
? ? ? ? ?condition)))))
(defun prompt-generator (&optional (num 0) (ctl-string “[~d] ”))
“Return a function that prints prompts like [1], [2], etc.”
#’(lambda () (format t ctl-string (incf num))))