Lisp-Stat 翻譯 —— 第四章 其它Lisp特性

第四章 其它Lisp特性

    上一章介紹了Lisp編程的基礎,在那章裏重點展現了對編寫Lisp函數有用的編程技術。爲了最高效地使用這些技術,知道Lisp和Lisp-Stat提供的函數和數據類型將是頗有用的。本章旨在給出這些函數和數據類型的概貌。初次閱讀,你能夠略讀,當你須要的時候再將其做爲參考手冊來使用。 node

4.1 輸入/輸出

Common Lisp編程語言包含一個可擴展的工具集,能夠用來讀寫文件、格式化輸出和自定義讀取輸入的方式。這些特徵的完整討論將佔用幾章內容,所以我只展現那些我發現的對統計編程有用的方面。更多的可擴展話題能夠到一些Common Lisp書籍中找到。 算法

4.1.1 Lisp讀取器

Lisp讀取器負責轉化類型化的字符,它經過用戶或者從文件中讀取的內容將數據輸出到Lisp數據項。當讀取一個數字時,讀取器負責識別是整數或者浮點數,而後將它轉換爲合適的內部表示。在Common Lisp裏,對浮點數據類型有一些選擇,能夠經過short-float、single-short、double-float和long-float等符號來指定。讀取浮點數用到的數據類型,好比說讀取2.0,由全局變量*read-default-float-format*控制,它的值是上邊提到的四個數據類型中的一個。強制指定要解釋的數字爲雙精度類型也是可能的,好比輸入成2.d0。 編程

讀取器還負責以一個特殊的方式解釋 一些字符,或者字符序列。這樣一個字符序列和用來解釋它的代碼,叫作一個讀取宏。咱們已經使用過一些讀取宏:引號字符"'",反引號字符"`",逗號字符",",和一對字符"#'"。爲了理解他們是如何工做的,咱們能夠將包含這些讀取宏的引號表達式輸入到解釋器,而後觀察返回了什麼: windows

> (quote 'x)
(QUOTE X)
> (quote `x)
(BACKQUOTE X)
> (quote ,x)
(COMMA X)
> (quote #'x)
(FUNCTION X)
例如,當讀取器看到一個引號字符"'"的時候,它將讀取下一個Lisp表達式,而後返回包含符號quot和表達式的一個列表。

    其它讀取宏還有雙引號字符",他將讀取緊跟着它的字符直到遇到下一個"爲止,並將它們造成一個字符串,反斜線字符\,它是針對與接下來有關聯的字符,用來轉義任何特殊意思的字符。例如,一個反斜線能夠用來產生一個包含雙引號的字符串。下面咱們將會遇到一些其它的讀取宏。 數組

    定義本身的讀取宏是可能的,經過這個方法,你能夠自定義Lisp讀取器來用你喜歡的語言語法來提供一個詞法分析器。 app

4.1.2 基礎打印函數

    爲了向標準輸出流或者一個文件裏打印數據,一些函數是可用的。最基本的函數是print、prin一、princ和terpri。print和prin都會打印它們的參數給一個form,這個form對Lisp讀取器來講是可讀的。若是能夠的話,讀取器應該可以讀取由這些函數打印的東西。print和prin1之間的不一樣是print的輸出的前邊加了一個空行,後邊跟了一個空格。princ和prin1類似,除了princ忽略了全部的轉義字符。特別地,princ打印的字符串不帶雙引號。函數terpri輸出一個新行。 less

    爲了展現這些函數,讓咱們看一下下邊定義的函數printer: 編程語言

> (defun printer ()
    (print "Hello")
    (print1 "this")
    (princ " is a")
    (print "string")
    (terpri))
PRINTER
它將產生如下輸出:
> (printer)

"Hello" "this" is a
"string" 
NIL
最後的nil是terpri返回的結果,也就是printer函數的返回值。

4.1.3 格式化

    若是你須要更多的輸出控制,你可使用format函數直接打印到標準輸出流上,或者輸出並創建一個輸出字符串。咱們暫且創建一個字符串,format函數有不少變量,可是咱們僅討論一些爲了咱們的目的有效的那些變量。 編輯器

    format函數與C語言裏的sprintf函數相似。想要造成一個字符串,這樣的format函數看起來像這樣:(format nil <control-string> <arg-1> ... <arg-n>),裏邊的control-string包含格式化指令,它以一個波浪線開頭,這些位置將被剩下的參數填滿。 ide

    最簡單的格式化指令是~a。它將被下一個參數替換,它被處理成就像被princ函數打印成的樣子。這有個例子:

> (format nil "Hello, ~a, how are you?" "Fred")
"Hello, Fred, how are you?"
~s指令與~a指令相似,不一樣的是它使用print-風格的格式化。使用~s,向Fred的問候可能像這樣:
> (format nil "hello, ~s, how are you?" "Fred")
"hello, \"Fred\", how are you?"
解釋器將使用print函數打印結果,它將在嵌入的引號前放置反斜線,表示它們將被做爲字面意思,不表示字符串的結尾。

    經過向~a和~s指令插入一個整數,你能夠指定域寬度,而後該參數將使用至少是你指定的域寬度進行打印,若是須要能夠比該寬度寬。在該寬度中參數是左對齊的:

> (format nil "Hello, ~10a, how are you?" "Fred")
"Hello, Fred      , how are you?"
若是你事先不知道你須要多大的寬度,你可使用字母v代替整數值,大小寫都可。而後format函數但願到下一個參數裏找到該整數,而後代替v的位置:
> (format nil "Hello, ~va, how are you?" 10 "Fred")
"Hello, Fred      , how are you?"
    爲了打印數字,對於整型數你可使用~d指令,對於浮點數字可使用~f、~e或者~g指令。~f指令使用十進制表示法,~e指令使用指數表示法,~g使用~e或者~f指令的表示法,選用它們兩個鍾短的那個。你能夠爲這些指令指定域寬度;在它們的域內,數字是右對齊的。對於三浮點指令(尾數大小爲3的浮點數),你能夠指定10進制數的小數點後的數字的位數,方法是方法是增長另外一個整數和它的逗號前導符。例如,在十進制表示法中使用域寬爲10,小數點後有三位數的格式來打印數字,有應該使用指令~10,3f。你也可使用指令~,3f忽略域寬度,你也可使用字符v替換指令裏的其中一個數字或者兩個都替換掉,而後在函數裏包含合適的整數做爲參數。這有幾個例子:
> (format nil "The value is: ~,3g" 1.23456)
"The value is: 1.23"
> (format nil "~10,2e, ~v,vf" 123.456 10 3 56.789)
"   1.23E+2,     56.789"
    還有兩個格式化指令是~~和~%,~~將插入一個~字符,~%將插入一個新行。最後,由波浪線~緊跟着一個新行的指令將致使format函數忽略新行和任何緊跟着的空白,直到第一個非空字符位置。該指令對於想另起一行來顯示較長的格式化命令字符串時是頗有用的:
> (format nil "A format string ~
written on two lines")
"A format string written on two lines"
    使用format函數創建一個字符串在構建統計圖形的標籤時是頗有用的。舉個例子,在第3.6.1節裏我定義了一個函數plot-expr,來繪製一個表達式的值相對於一個特定變量的值的圖形。仙子阿咱們能夠針對該圖,使用表達式和變量符號,來修改這個函數去構建變量的標籤:
> (defun plot-expr (expr var low high)
    (flet ((f (x) (eval `(let ((,var ,x)) ,expr)))
           (s (x) (format nil "~s" x)))
      (plot-function #'f low high
                     :labels (list (s var) (s expr)))))
PLOT-EXPR
局部函數s使用~s格式化指令將他的參數轉化爲一個字符串。plot-function函數的:label關鍵字參數帶一個字符串列表,用來做爲座標的標籤。

    format函數不光能夠用來創建字符串。若是format函數的第一個參數的nil被t代替的話,format函數將向標準輸出流打印數據,而後返回nil。例如,你可使用format函數打印一張小表:

> (format t 
          "A Small Table~%~10,3g ~10,3g~%~10,3g ~10,3g~%"
          1.2 3.1 4.7 5.3)
A Small Table
  1.20       3.10    
  4.70       5.30    
NIL

4.1.4 文件和流

Lisp的I/O函數從流裏讀取數據並向流寫入數據。默認的用於輸出的流是與全局變量*standard-output*綁定的;相應的默認的輸入流是*standard-input*。經過給定一個流做爲第二個參數,print函數可被告知使用哪一個流。經過給定一個流做爲一個參數,而不是用t或者nil,format函數也能夠被通知使用一個流來打印數據。

文件流

流能夠以一些方式造成。一個方式就是使用open函數打開一個文件,open函數帶一個文件名做爲參數,而後返回一個流。默認地,這個流是一個輸入流。爲了打開一個用於輸出目的的流,你能夠給定open命令一個附加的關鍵字參數:direction,它的值應該是另外一個關鍵字,:input表示爲輸入文件而創建,它是默認值,:output表示爲輸出文件而創建。這裏有一個例子,關於你如何寫一些簡單的表達式給一個名爲"myfile"的文件。若是由這個文件名命名的文件已經存在,先前存在的文件將被覆寫。不然,新文件將被創建:

> (setf f (open "myfile" :direction :output))
#<Character-Output-Stream 5:"e:\lisp-stat\myfile">
> (print '(+ 1 2 3) f)
(+ 1 2 3)
> (format f "~%~s~%" 'Hello)
NIL
> (close f)
T
函數close將關閉一個文件流。由於大多數系統都限制能夠同時打開的文件的數量,所以一旦你不須要一個文件的時候,記得關閉它是很重要的。

    文件的內容以下:

咱們可使用read函數將這個文件的內容再讀回到Lisp裏:

> (setf f (open "myfile"))
#<Character-Input-Stream 5:"e:\lisp-stat\myfile">
> (read f)
(+ 1 2 3)
> (read f)
HELLO
read函數每次從指定流裏讀取一個表達式。

    若是流是空的,或者已經到達文件尾,read函數將發出一個錯誤信號:

> (read f)
Error: end of file on read
Happened in: #<Subr-READ: #13e6284>
爲了探測到文件尾而不引發一個錯誤,咱們能夠用值nil對read函數的第二個參數賦值。咱們還能夠爲read函數給定第三個參數,當到達文件尾時該isp數據項將返回出來。若是沒有提供這個第三個參數,默認值將是nil:
> (read f nil)
NIL
> (read f nil '*eof*)
*EOF*
    打開一個文件,將它的流賦值給一個變量,運行一些操做,而後關閉流,這一操做模式是至關廣泛的。所以Lisp提供了一個宏來簡化這個處理過程。該宏這樣調用:(with-open-file (<stream> <open args>) <body>)。參數<stream>是一個符號,用來與流關聯,<open args>是open函數的參數,<body>表示使用這個流進行計算的表達式。爲了使用with-open-file寫咱們的文件,咱們將這樣使用:
> (with-open-file (f "myfile1" :direction :output)
                  (print '(+ 1 2 3) f)
                  (format f "~%~s~%" 'Hello))
NIL
with-open-file宏在返回以前關閉了文件流。尤爲是,當對內部的表達只求值時即便發生了錯誤,它也能確保文件流關閉。

    在處理文件I/O時偶爾有用的兩個函數是force-output和file-position。force-output函數帶一個可選的流參數,而後爲流刷新輸出緩衝區。若是沒有提供參數,流默認將向標準輸出裏輸出。file-position函數帶一個流參數和一個可選的整型參數,這個流參數將與一個文件綁定。在僅使用流參數調用file-position函數時,它將返回針對這個文件流的文件指針的當前位置。當將一個整型數賦給函數做爲第二個參數時,函數將文件指針設置到這個整型數字指定的那個位置。這個操做能夠用來實現文件回讀和文件I/O隨機訪問。

字符串流

能夠構建一個流,來創建和分解字符串。假設你已經得到一個包含Lisp表達式的字符串,好比"(+ 1 2)"。這樣的一個字符串能夠從一個對話框窗口的文本域獲取,你可使用宏with-input-from-string從字符串裏讀取該表達式,這個宏這樣調用:(with-input-from-string (<stream> <string>) <body>)。它使用字符串<string>建立了一個字符串輸入流,並和符號<stream>綁定,而後在<body>裏使用這個綁定對錶達式求值。返回的結果就是<body>裏最後一個表達式的求值結果。你可使用如下函數從字符串裏提取表達式:

> (with-input-from-string (s "(+ 1 2)") (read s))
(+ 1 2)
宏with-output-to-string容許你使用print函數和其它輸出函數創建一個字符串。它能夠這樣調用:(with-output-to-string (<stream>) <body>)。它建立了一個字符串輸出流,並和符號<symbol>綁定,使用這個綁定對<body>表達式求值,在全部的表達式被處理完後返回包含在流裏的字符串:

4.2 定義更多靈活的函數

目前,咱們遇到的一些內建函數或者是帶可選參數的,或者是帶關鍵字參數的,或者容許使用任意數量的參數。在你自定義的函數裏也能夠包括這些特性。

4.2.1 關鍵字參數

經過在參數列表裏放置一個&key,你就定義了一個帶關鍵字參數的函數。&key後邊的全部參數都指望以關鍵字參數的形式來提供。舉個例子,假設你想要遞歸factorial函數的一個版本,它以遞歸處理打印參數的當前值。這可使用關鍵字參數實現:

> (defun factorial (n &key print)
    (if print (format t "n = ~d~%" n))
    (if (= n 0) 1 (* (factorial (- n 1) :print print) n)))
FACTORIAL
不使用關鍵字參數,factorial只不過計算factorial而後返回計算後的值:
> (factorial 3)
6
可是若是提供一個非nil值給關鍵字參數,factorial將打印附加信息:
> (factorial 3 :print t)
n = 3
n = 2
n = 1
n = 0
6
    關鍵字參數也是可選的,若是不提供關鍵字參數,它默認爲nil。經過對這個默認值而不只是一個符號給出參數列表和表達式,咱們能夠替換這個默認值。若是咱們像這樣定義factorial函數:
> (defun factorial (n &key (print t))
    (if print (format t "n = ~d~%" n))
    (if (= n 0) 1 (* (factorial (- n 1) :print print) n)))
FACTORIAL
那麼,該默認值將打印參數,而後不得不經過向關鍵字參數:print傳遞nil來關閉打印:
> (factorial 3)
n = 3
n = 2
n = 1
n = 0
6
> (factorial 3 :print nil)
6
    儘管你很願意使用默認值nil,有時你可能喜歡可以區分一個忽略的關鍵值和使用nil爲參數的關鍵值之間的不一樣。這點能夠經過向關鍵字符號加入另外一個符號和它的默認表達式加入另外一個符號來完成。這個符號叫作supplied-p參數,若是提供了關鍵字該值將被設成t,不然將被設爲nil。舉個簡單的例子,讓咱們定義一個函數:
> (defun is-it-there (&key (mykey nil is-it)) is-it)
IS-IT-THERE
而後咱們不帶關鍵字參數和帶關鍵字參數分別調用一下:
> (is-it-there :mykey nil)
T
> (is-it-there)
NIL
    默認狀況下,用來表示關鍵字參數的關鍵字僅僅是在參數符號前價格冒號。有時,使用一個長關鍵字是頗有用的,可是爲了可以在函數體裏使用一個短符號引用關鍵字參數,咱們能夠提供一個關鍵字參數對,經過用一個關鍵字和一個符號組成的列表再加上該列表的默認值,去替換原來的參數符號來達到這個目的。在咱們的factorial例子裏,爲了使用關鍵字:print卻調用參數pr,咱們能夠這樣用:
> (defun factorial (n &key ((:print pr) t))
    (if pr (format t "n = ~d~%" n))
    (if (= n 0) 1 (* (factorial (- n 1) :print pr) n)))
FACTORIAL
    關鍵字參數的默認表達式被計算的那個環境,是由函數定義的那個環境和綁定函數的參數列表的全部參數的那個環境組成的,是參數初始化以前的那個參數列表。使用一個let*結構,這個初始化過程能夠認爲是隱式的。

    函數能夠帶多個關鍵字參數,關鍵字參數能夠以任意順序被使用,只須要跟在須要的參數後邊。

4.2.2 可選參數

在咱們的factorial例子中,咱們可使用一個可選參數代替關鍵字參數。方法是類似的:使用&optional符號代替參數列表裏的&key符號:

> (defun factorial (n &optional print)
    (if print (format t "n = ~d~%" n))
    (if (= n 0) 1 (* (factorial (- n 1) print) n)))
FACTORIAL
不帶可選參數,factorial僅返回結果:
> (factorial 3)
6
爲可選參數傳遞一個非nil值,每次調用時都會打印參數值:
> (factorial 3 t)
n = 3
n = 2
n = 1
n = 0
6
    與關鍵字參數相似,可選參數默認值爲nil可是能夠替換其默認值。方法與關鍵字參數相關操做是同樣的。爲了使print的默認值爲t,咱們能夠這樣:
> (defun factorial (n &optional (print t))
    (if print (format t "n = ~d~%" n))
    (if (= n 0) 1 (* (factorial (- n 1) print) n)))
FACTORIAL
supplied-p參數也能夠像關鍵字參數裏那樣工做。

    與關鍵字參數不一樣的是,若是一個函數帶多於一個的關鍵字參數,那麼這些可選參數必須以他們在函數定義裏指定的順序使用。若是一個函數裏既使用了可選參數也是用了關鍵字參數,那麼在函數定義時可選參數必需要在關鍵字參數前邊。在使用同時帶可選參數和關鍵字參數的函數時,若是你想使用任何關鍵字參數,必須指定全部的可選參數。

4.2.3 參數的變量數量

在第3.7節定義的那個簡單的矢量加法函數vec+僅須要兩個參數,而內建的加法函數+容許任意多的參數。經過在參數列表裏使用&rest符號,咱們能夠修改vec+的定義來容許任意數量的參數。若是一個函數的參數列表裏包含&rest符號,那麼在一次函數調用裏的剩餘的參數將被組合到一個列表裏,而後被綁定到&rest符號後邊的那個符號上,這裏有個vec+的修改版本:

> (defun vec+ (&rest args)
    (if (some #'compound-data-p args)
        (apply #'map-elements #'vec+ args)
        (apply #'+ args)))
VEC+
在函數體裏,變量args將與在vec+函數調用時使用的全部參數進行關聯。函數some帶一個謂詞函數和一個或多個列表,而後它將映射該謂詞到一個或多個列表,直到返回一個非nil結果或者遍歷過全部元素。這個定義也使用了在第3.6節中描述的那個apply函數的更精巧的版本。

    可選參數、剩餘參數和關鍵字參數全都出如今同一個參數列表裏,若是他們的多個出現了,那麼必須以&optional, &rest, &key的順序出現。

4.3 控制結構

4.3.1 條件求值

咱們已經普遍使用了條件求值結構cond和if。另外一個條件求值結構是case,這在第3.10.2節作過簡介。case能夠用來根據經過表達式返回值的離散集合來選擇相應動做,case表達式的通常形式是:

(case <key expr>

    (<key 1> <expr 1>)

    ...

    (<key n> <expr n>))

<key expr>被求值,而後每次與一個<key i>表達式進行比較,<key i>表達式多是數字、符號、字符或者是他們的列表形式。若是<key expr>的值與一個鍵或者一個鍵列表的一個元素匹配,那麼相應的結果表達式將被求值,而後它的結果被返回做爲case表達式的結果。與cond語句類似,case語句可能包含一系列的結果表達式。這些表達式會被求值,一次只求值一個表達式,最後一個表達式的結果將返回。最後一個case語句可能以符號t開始,這種狀況下若是其它語句都沒被使用,最後一條語句將被用到。沒有默認語句的話,若是沒有一條語句是匹配的,case表達死將返回nil。舉個簡單的例子,下邊的表達式:

> (case choice
    ((ok delete) (delete-file))
    (cancel (restore-file)))
該語句可能用來相應用戶作出的選擇,這是用戶選擇來自一個對話框的菜單或者按下按鈕後,選擇了一個數據項引發的。

    偶爾地,若是一個謂詞是or或者是非真,可以對一系列表達式求值就頗有用了。這能夠經過使用cond來完成,可是Lisp也提供了兩個簡單的替代物when和unless。表達式(when <p> <expr 1> ... <expr n>)和(unless <p> <expr 1> ... <expr n>)與(cond (<p> <expr 1> ... <expr n>))和(cond (<p> nil) () <expr 1> ... <expr n>))是分別等價的。

4.3.2 循環

目前爲止,可用的最靈活的Lisp迭代結構是do,這在3.4節介紹過了。do事實上有比如今更多的選項。尤爲是,它容許一系列的結果表達式跟在終端測試以後,也容許一系列的表達式被當作循環體給loop。循環體會使用do變量的當前綁定在每次迭代總執行。do表達式最通常的形式是:

(do ((<name 1> <initial 1> <update 1>)
     ...
     (<name n> <initial n> <update n>))
    (<test> <result 1> ... <result k>)
    <expr 1>
    ...
    <expr m>)
對於產生反作用像賦值或者打印,額外的結果和循環體表達式是有用的。例如,咱們能夠修改3.4節的my-sqrt函數的定義成這樣:
> (defun my-sqrt (x &key print)
    (do ((guess 1 (improve guess x)))
        ((goog-enough-p guess x)
         (if print (formet t "Final guess: ~g~%" guess))
         guess)
        (if print (format t "Current guess: ~g~%" guess))))
MY-SQRT
爲了在迭代過程當中提供一些信息:
> (my-sqrt 3 :print t)
Current guess: 1.
Current guess: 2.
Current guess: 1.75
Error: The function FORMET is not defined.
Happened in: #<FSubr-DO: #140387c>

    do設置的變量綁定是並行設置的,就像使用let進行局部變量綁定同樣。宏do*與do相似,除了一點就是它構建的綁定是串行的,所以這與let*是類似的。

    一些簡單的迭代結構也是可用的,有兩個這樣的結構dotimes和dolist,這在第2.5.6節簡單介紹過。宏dotimes這樣調用:

(dotimes (<var> <count> <result>) <body>)
當對一個dotimes表達式求值時,<count>表達式首先求值,它應該求值成一個整數。那麼對於從零(包括零)到<count>(不包括count)之間的每一個整型計數都會按順序與變量<var>綁定,而後<var>每變一次,<body>裏的表達式都會進行一次求值。若是<count>的求值結果爲零或爲負值,<body>裏的表達式再也不求值。最後,<result>表達式將被求值,而後返回結果。<result>表達式是可選的,不過不提供該表達式,dotimes表達式的值將默認爲nil。

    在dotimes裏使用結果表達式,咱們能夠修改第3.8節裏給出的factorial函數的C版本代碼的翻譯版本成這樣:

(defun factorial (n)
    (let ((n-fac 1))
      (dotimes (i n n-fac)
               (setf n-fac (* n-fac (+ i 1))))))
    dolist宏與dotimes宏相似。它這樣調用:
(dolist (<var> <list> <result>) <body>)
而後對<list>的每個元素都重複它的dolist體。使用dolist宏,你能夠編寫一個簡單的函數來累加一個列表裏的全部元素:
(defun sum-list (x)
    (let ((sum 0))
      (dolist (i x sum)
              (setf sum (+ sum i)))))
    一個很是簡單的迭代結構是loop,它的通常形式是(loop <body>)。它無限重複,每次循環的時候都按順序執行<body>裏的每條表達式。爲了中止該循環,你能夠在loop裏包含一個return表達式。return表達式帶一個可選參數,它將做爲loop表達式的值,它的默認值爲nil。還有另外一個版本的factorial函數:
(defun factorial (n)
    (let ((i 1)
          (n-fac 1))
      (loop (if (> i n) (return n-fac))
            (setf n-fac (* i n-fac))
            (setf i (+ i 1)))))
return表達式也能夠在dotimes、dolist、do和do*結構裏使用。若是這些循環的一個是因爲調用了return表達式而結束的,那麼標準返回表達式將不被求值。

4.4 基本Lisp數據和函數

到目前爲止,咱們普遍使用的Lisp數據類型僅是數值、字符串、符號和列表。本節將複習這些數據類型而後介紹一些新的數據類型,尤爲是矢量和多維數組。

4.4.1 數值型

Lisp提供了一些不一樣的數值類型,包括整型、浮點型和複數型數值。Common Lisp也包含一個有理數數據類型。對於任何Lisp數值,謂詞numberp都將返回t。謂詞integerp和floatp能夠用來識別整型數和浮點數。謂詞complexp可識別複數數值。

在實數數值類型之間轉換

函數floor、ceiling、truncate和roung能夠用來將浮點數或者複數轉換爲與其接近的整型數,當對它們賦值整型數當參數時,它們只簡單地返回它們的參數。在Lisp-Stat裏,全部這四個函數都是矢量化的,floor老是返回一個接近參數可是比參數曉得整數,而truncate向零值處截斷。

    使用float函數強制將整型數和有理數轉化爲浮點數是可能的。若是float函數做用在一個浮點數上,那麼那個數字將簡單返回。在Common Lisp裏,若是float函數做用到一個整型數或者有理數上,那麼,默認地,將返回一個單精度浮點數。爲了將一個不一樣的數據類型強制轉換爲浮點數,你能夠賦給float函數第二個參數,一個想要的類型的數字。那麼表達式爲:

> (float 1 0.d0)
1.0
它將返回一個雙精度浮點數。在Lisp-Stat裏,float函數是矢量化的,因此:
> (float (list 1 2 3) 0.d0)
(1.0 2.0 3.0)
將返回一個雙精度數值的列表。

    在Common Lisp裏,當給超越函數傳遞參數時,好比log函數和sin函數,整型數和有理數數類型將被強制成single-float類型的浮點數值。若是在你的Lisp系統裏,浮點類型不能提供足夠的足夠的精度。在用這些函數前要將你的數據強制轉換爲雙精度數值。

複數數值

複數數值將使用下邊這個形式的表示方法打印:

#C(<realpart> <imagpart>)
其中的<realpart>表明數值的實部,<imagpart>表明虛部。例如,
> (sqrt -1)
#C(0.0 1.0)
爲了直接構建一個複數,你也可使用complex函數,就像這樣:
> (complex 3 7)
#C(3 7)
或者你能夠它的打印表達式:
> #C(3 7)
#C(3 7)
在Lisp-Stat裏,complex函數是矢量化的。

    這兩種方法之間有一些細微的不一樣。函數complex是一個普通函數,咱們能夠如下邊的表達式做爲它的參數來調用它:

> (complex (- 2 3) 1)
#C(-1 1)
相反地,字符對#C或者#c,造成了一個讀取宏。這個讀取宏將指示Lisp讀取器讀取下一個表達式,這個表達式必定是兩個實數的列表,而後分別使用那些數字做爲實部和虛部,來構建一個複數。這個列表將被當作是字面量,該列表和其參數不會被求值。結果,實部和虛部將以數字的形式給出,而不是以求值成數字的表達式的形式給出。試圖將一個數字替換成一個表達式將產生一個錯誤:
> #c((- 2 3) 1)
Error: not a real number - (- 2 3)
Happened in: #<Subr: #12a4f30>
本章咱們還會遇到一些其它的讀取宏,它們都將分享不對它們的參數求值這一特性。

    與實數相似,複數數值能夠是整數、有理數或者浮點數。若是傳遞給complex函數的連個參數中有一個是浮點數,那麼另外一個也將被轉換爲浮點數,結果將是一個復類型的浮點數。若是這兩個參數都是整數,結果將是復類型的整數。復類型的整數一般有一個非零虛部。虛部爲零的復類型整數將被轉換爲實整型數。換句話說,復類型的浮點數能夠有一個值爲零的虛部:

> #c(1 0)
1
> #c(1.0 0.0)
#C(1.0 0.0)
    複數數值的笛卡爾表示法和極座標表示法的組件可使用realpart函數、imagpart函數、phase函數和abs函數來提取:
> (realpart #C(3 4))
3
> (imagpart #C(3 4))
4
> (phase #c(3 4))
0.9272952180016122
> (abs #C(3 4))
5.0
在Lisp-Stat裏,這些函數都是矢量化的。

4.4.2 字符串和字符

咱們已經在不少例子裏使用過字符串了,它們被寫成包含在一對雙引號內部的字符序列。字符是組成字符串的元素。他們(字符串)被當作是一種特殊的數據類型,與單元素字符串和數值型字符代碼相區別。爲了弄清楚Lisp如何打印一個字符,咱們可使用函數char從字符串裏提取一個字符,字符串的索引從零開始。那麼字符串"Hello"裏的字符的下標分別是0、一、二、3和4:

> (char "Hello" 0)
#\H
> (char "Hello" 1)
#\e
    字符序列"#\",來自於一個讀取宏,該讀取宏用於讀取和打印字符,#\與咱們前邊用過的用來表示複數的那個符號很是類似。這還有一些例子:
> #\a
#\a
> #\3
#\3
> #\:
#\:
> #\newline
#\Newline
標準的可打印的字符,好比字母、數字、或者標點符號,均可以經過在#\的後邊加一個合適的字符的形式來表示。特殊字符,好比newline字符,一般會使它的名字超過一個字符的長度。一些系統有額外的非標準字符,這些在其它系統裏是找不到的,例如,在蘋果Macintosh操做系統的XLISP-STAT裏,apple字符使用#\Apple表示。

    使用string函數,一個字符能夠轉換爲只有單個字符的字符串:

> (string #\H)
"H"
    字符串和字符能夠分別經過stringp和character兩個謂詞來識別。

4.4.3 符號

與其它大多數語言不一樣,Lisp符號是真正的在計算機內存中佔用必定空間的物理對象。符號包含4個部分:

  • 一個打印名
  • 一個值單元
  • 一個函數單元
  • 一個屬性列表

由於本書的剩餘部分都不會使用屬性列表,因此我不會進一步地討論它。

    符號的值單元包括由符號命名的全局變量。若是值單元裏不包含一個數值,這個符號就是未綁定的,當你試圖去取該符號的值時,將引起一個錯誤。謂詞boundp能夠用來確認一個符號是否有一個全局的值。一個符號的函數單元包含符號的全局函數定義,固然若是有的話。謂詞fboundp將肯定一個符號是否有一個全局函數定義。

    一個符號的單元裏的內容可使用函數symbol-name,symbol-value和symbol-function來提取:

> (symbol-name 'pi)
"PI"
> (symbol-value 'pi)
3.141592653589793
> (symbol-function 'pi)
Error: The function PI is not defined.
Happened in: #<Subr-TOP-LEVEL-LOOP: #13e2ee0>
符號pi沒有函數定義,因此讀取它的函數單元的時候,引起了一個錯誤。

    使用setf函數,讀取函數symbol-name,symbol-value和symbol-function能夠被用來做爲通常性的變量。例如,咱們能夠設置一個符號的值:

> (setf (symbol-value 'x) 2)
2
> x
2
或者設置一個符號的函數定義:
> (setf (symbol-function 'f) #'(lambda (x) (+ x 1)))
#<Closure: #141d194>
> (f 1)
2

    使用符號做爲變量和函數的標籤時,咱們一般把這些符號與它們的打印名稱視爲是等價的,對大多數目的來講,這樣作是合理的。當讀取函數須要一個符號來表示一個特殊的打印名時,若是那個名字表示的符號已經不存在了,那麼它僅僅構建一個新的符號而已。結果,帶有相同打印名的符號應該是eq,然而,使用setf和symbol-name函數來改變一個符號的打印名稱也是可能的,這麼作可能會致使讀取函數混亂並致使不可預知的結果,應該禁止這麼作。

4.4.4 列表

列表是經過初級數據構建複雜數據的基本工具。雖然列表的結構是順序性的,它們也能夠用來表示數據項的無序集合。

圖4.1 列表(a b c)的cons cells圖表

基本列表結構

最基本的列表就是空列表nil,也叫null列表。它能夠寫成nil或者()。它能夠自求值:

> nil
NIL
> ()
NIL
    一個非空列表是由一系列的cons cells組成的。一個cons cells能夠被視爲由兩個域組成。一個域指向列表的一個元素,另外一個域指向列表的剩餘部分,或者說指向列表剩餘部分裏的下一個cons cells,或者指向nil。圖4.1展現了一個形式(a b c)的cons cells的組成的圖表。

    cons cells的兩個域的內容可使用函數first和rest來提取:

> (first '(a b c))
A
> (rest '(a b c))
(B C)
> (rest (rest '(a b c)))
(C)
> (rest (rest (rest '(a b c))))
NIL
函數cons構建了一個新的cons cell,它的域指向它的兩個參數:
> (cons 'a '(b c))
(A B C)
> (cons 'C nil)
(C)
> (cons 'a (cons ' b (cons 'c nil)))
(A B C)
    cons只不過構建了一個新的cons cell;它的第二個參數沒有拷貝列表,若是咱們這樣構建一個列表x的話:
> (setf x '(a b c))
(A B C)
而後構建第二個列表y:
> (setf x '(a b c))
(A B C)
> (setf y (cons 'd (rest x)))
(D B C)
那麼,列表x與列表y僅是cons cell中的第一個元素不一樣,它們剩餘部分相同的cons cell組成,所以是eq等價的:
> (eq x y)
NIL
> (eq (rest x) (rest y))
T
那麼,破壞性地改變x的第一個元素將不會影響y,可是,改變x的第二個或者第三個元素將會改變y的相應元素。

   在第2.4.4節裏介紹的函數append,最後一個參數重用了cons cell。也就是說,表達式(append '(1 2) y)返回的結果的最後三個cons cell與組成y列表的cons cell是相同的:

> (append '(1 2) y)
(1 2 D B C)
> (eq y (rest (rest (append '(1 2) y))))
T
append函數所作的重用cons cells的主要的優點是節約了內存,劣勢是在破壞性地進行列表修改的時候將致使不可預知的負面效果。

將列表做爲集合

在使用列表表示數據項集合方面,有一些函數是可用的。函數member測試一個元素是否在一個列表之中:

> (member 'b '(a b c))
(B C)
> (member 'd '(a b c))
NIL
若是元素找到了,那麼member函數將返回列表的剩餘部分而不是返回符號t,這個列表的第一個元素就是咱們要找的那個元素。由於Lisp解釋器將凡是非nil的都當成true,這不會引發任何問題,而且一般是有用的。

    adjoin函數將一個元素加到列表前邊,除非這個元素已經在該列表裏了:

> (adjoin 'd '(a b c))
(D A B C)
> (adjoin 'd '(d a b c))
(D A B C)
    union和intersection函數分別返回參數列表裏的兩個參數的並集和交集:
> (union '(a b c) '(c d))
(D A B C)
> (intersection '(a b c) '(c d))
(C)
只要全部的參數都不包含重複項,結構也將不包含任何重複項。

    set-difference返回了包含在第一個參數列表而不包含在第二個參數列表裏的那些元素:

> (set-difference '(a b c) '(c d))
(B A)
函數union、intersection和set-difference都不保證保存元素會按他們在參數列表裏出現的順序出現。

    這些函數須要可以肯定是否一個列表裏的兩個元素被視爲是等價的。默認地,它們使用謂詞eql來測試等價性,可是你可使用關鍵字:test來指定一個備用的測試,例如:(member 1.0 '(1 2 3))返回false,由於整數1和浮點數1.0不是eql等價的。換句話說,(member 1.0 '(1 2 3) :test #'equalp)返回true,由於1與1.0是equalp等價的。你能夠提供任意的帶兩個參數的函數做爲測試謂詞。

    須要比較Lisp數據項的一些其它函數也容許經過使用:test關鍵字來指定備用的謂詞。兩個例子就是函數subst和remove。

4.4.5 向量

目前爲止,咱們已經將數值型數據集表示爲列表形式,咱們也能夠將它們表示成向量的形式。向量可使用相似下邊的表達式來構建:

> '#(1 2 3)
#(1 2 3)

字符#(是一個讀取宏,表示向量的起始,接下來的表達式,一直到匹配的),這些都將被讀取函數讀入並造成一個向量,不會被求值。這裏須要引號,由於對一個向量求值時沒有意義的。

    你也可使用vector函數,將它的參數構形成一個向量:

(vector 1 2 3)
#(1 2 3)
    函數length將返回一個向量的長度。使用select或者elt函數,能夠提取向量裏的元素。謂詞vecotrp能夠用來測試一個lisp項是否是向量:
> (setf x (vector 1 'a #\b))
#(1 A #\b)
> (length x)
3
> (elt x 1)
A
> (select x 1)
A
> (vectorp x)
T
與select不一樣的是,elt不容許經過給定的列表或者向量的下標來提取子向量。然而elt函數在不少實現版本里都比select更加高效。那麼你可能想要在代碼裏使用elt函數,在速度上將是很好的優化。 

    elt和select函數均可以經過使用setf被當作通常變量來使用。

    是否將數據集表示成列表比表示成向量更好,還不是太清楚。向量確實提供了一些超過列表的優點,尤爲地,讀取向量上特定位置的元素所須要的時間是與改元素所在的位置無關的。相反,列表的元素經過遍歷列表的結構單元來查找所要的元素,直到改元素被找到。若是將一個數據集表示爲向量的形式,那麼以隨機順序獲取該數據集中元素的函數將運行得更快一些。另外一方面,Lisp提供的處理列表的函數比處理向量的函數要多。在一些lisp系統裏列表在使用內存方面也能夠比向量更加高效。

4.4.6 序列

Common Lisp有不少好書,它們能夠應用與列表、向量或者字符串上。函數length就是一個例子:

> (length '(1 2 3))
3
> (length '#(a b c d))
4
> (length "hello")
5
術語sequence用來描述這兩三種數據類型的並集。

    函數elt用來提取序列裏的一個元素:

> (elt '(1 2 3) 1)
2
> (elt '#(a b c d) 2)
C
> (elt "Hello" 4)
#\o
你可使用函數coerce將一個序列從一個類型強制轉換爲另外一個類型。例如:
> (coerce '(1 2 3) 'vector)
#(1 2 3)

若是傳遞給coerce函數的第一個參數是指定的類型的話,它是否能夠被拷貝,取決於所用的Lisp系統。若是你確實想拷貝一個序列的話,你可使用函數copy-seq。這個函數僅拷貝序列,而不是序列的元素。元是序列和拷貝後的序列是逐元素eq等價的。若是函數coerce的結果類型符號是string類型的,可是序列的元素不都是字符,coerce函數將發出錯誤信號。

    concatenate函數帶有一個結果類型符號,而且帶有任意數量的序列做爲參數,它將返回一個新的序列,這個序列按順序包含參數序列的全部元素:

> (concatenate 'list '(1 2 3) #(4 5))
(1 2 3 4 5)
> (concatenate 'vector '(1 2 3) #(4 5))
#(1 2 3 4 5)
> (concatenate 'string '(#\a #\b #\c) #(#\d #\e) "fgh")
"abcdefgh"
與append函數不一樣的是,concatenate函數不會重用列表的部分元素,全部的序列都是拷貝的。和corece函數同樣,若是結果類型的符號是string而序列的元素不都是字母的狀況下,concatenate函數將發出一個錯誤信號。

    函數map能夠用來將列表和矢量進行函數映射,它的第一個參數必須是一個指定函數返回結果的符號:

> (map 'list #'+ (list 1 2 3) #(4 5 6))
(5 7 9)
> (map 'vector #'+ (list 1 2 3) #(4 5 6))
#(5 7 9)
跟在函數後邊的參數可使列表、矢量或者字符串的任意組合。

    函數some對一個或多個序列逐元素地使用一個謂詞,直到該謂詞知足條件或者最短的那個序列用完爲止:

> (some #'< '(1 2 3) '(1 2 4))
T
> (some #'> '(1 2 3) '(1 2 4))
NIL
函數every也是相似的。

    另外一個有用的序列函數是reduce,這個函數帶一個二進制函數和一個序列做爲參數,而且使用二進制函數將序列的元素組合起來。例如,你可使用下邊的表達式將一個序列的元素逐個相加:

> (reduce #'+ '(1 2 3))
6
你可使用關鍵字:initial-value給reduce函數一個初始值。使用帶這個參數的reduce函數,你能夠翻轉列表裏的元素:
> (reduce #'(lambda (x y) (cons y x))
          '(1 2 3)
          :initial-value nil)
(3 2 1)

傳遞給reduce函數的第一個參數是得到的目前爲止的減小量,第二個參數是序列裏的下一個元素。

    函數reverse返回一個列表,這個列表是所給參數的元素的逆序列:

> (reverse '(a b c))
(C B A)
    函數remove帶兩個參數,一個是元素,一個是序列,而後返回一個新的序列,該序列是由全部不等於第一個參數的元素組成的,新序列各元素的順序與原序列相同。remove-duplicates函數帶一個序列做爲參數,而且返回一個新的序列,該新序列移除了全部重複元素。若是一個元素在參數序列裏出現不止一次,那麼除了最後一個元素以外的其它元素都會被移除。這些函數使用的默認的相等測試函數是eql函數,使用:test關鍵字後可使用備用測試函數。 

    函數remove-if帶有一個單獨的參數謂詞和一個序列做爲參數,返回一個新的序列,該序列中全部知足謂詞的元素都被移除掉了。例如:

> (remove-if #'(lambda (x) (<= x 0)) '(2 -1 3 0))
(2 3)
remove-if-not與之類似,只不過謂詞的結果是相反的。

    使用remove做爲函數名起始部分的都是非破壞性函數。他們產生一個新的序列而不修改或者重用參數。相似地,以delete做爲函數名起始部分的函數是破壞性函數,它們修改或重用參數的數據,而且能夠在內存利用上得到更高的效率。

    函數find帶一個元素和一個序列做爲參數,返回與find函數的第一個參數匹配那個元素,這個元素存在於find函數的第二個參數序列裏。

> (find 'b '(a b c))
B
> (find 'd '(a b c))
NIL
position函數與find函數的參數列表相同,它返回與參數列表的第一個參數相匹配的那個元素的下標,若是沒有匹配值則返回nil:
> (position 'b '(a b c))
1
> (position 'd '(a b c))
NIL
find函數和position函數都是用eql做爲它們默認的謂詞測試函數,若是想使用備用的測試函數,能夠用:test關鍵字來指定。

    Common Lisp提供了一個強大的排序函數sort函數。sort函數須要兩個參數,一個序列和一個謂詞。謂詞應該是一個函數,該函數帶兩個參數,而且僅當第一個參數嚴格小於第二個參數的時候返回非nil結果。sort函數應該謹慎使用,由於它會破壞性地對序列進行排序。也就是說,因爲效率的緣由,在運行排序算法的時候,它將部分地重用參數的數據,在處理過程當中破壞了原來的序列。那麼若是你想保護參數,在你將它傳遞給sort函數以前要進行數據拷貝。相反地,第2.7.3節裏介紹的sort-data函數是非破壞性的函數。

4.4.7 數組

向量是一維的數值。Lisp還提供了多維數組。

數組的構建

make-array函數用來構建一個數組。它須要一個參數,數組的維度列表。例如:

> (make-array '(2 3))
#2A((NIL NIL NIL) (NIL NIL NIL))
它返回的結果是一個2行3列的二維數組,全部的元素都是nil。該對象被打印成這樣:首先是一個#號,後邊跟着改數組的維數/階數,而後是字符A,最後是每一個數據行的列表。一個三維數組能夠這樣建立:
> (make-array '(2 3 4))
#3A(((NIL NIL NIL NIL) (NIL NIL NIL NIL) (NIL NIL NIL NIL)) ((NIL NIL NIL NIL) (NIL NIL NIL NIL) (NIL NIL NIL NIL)))
在它的打印結果裏,數據元素被第一個維度分裂開,而後是第二個維度,最後是第三個維度。

    一個向量也可使用make-array函數來構建:

> (make-array '(3))
#(NIL NIL NIL)
針對向量的維度列表只有一個元素,爲方便起見,能夠直接給make-array傳遞一個數值型參數
> (make-array 3)
#(NIL NIL NIL)
    make-array能夠帶一些關鍵字參數。:initial-element關鍵字能夠用來使用一個初始量構建數組而不使用nil:
> (make-array '(2 3) :initial-element 1)
#2A((1 1 1) (1 1 1))
:initial-contents關鍵字讓你使用一個相似打印體的表示方法來指定很是數數組的內容:
> (make-array '(3 2) :initial-contents '((1 2) (3 4) (5 6)))
#2A((1 2) (3 4) (5 6))
make-array函數的另外一個關鍵字參數是:displaced-to。若是提供了這個關鍵字,它的參數必須是一個數組,它所含的元素的總數與指定的維度列表的元素總數相同。返回的新的數組就是所謂的 替換數組或者 共享數組。它沒有本身的數據而只是和提供給它參數的那個數組共享數據。這兩個數組之間的不一樣是獲取元素、打印數組等方面所使用的維數。這裏有個例子:
> (setf x (make-array '(3 2)
                      :initial-contents '((1 2) (3 4) (5 6))))
#2A((1 2) (3 4) (5 6))
> (make-array '(2 3) :displaced-to x)
#2A((1 2 3) (4 5 6))
爲了理解這個結果,你須要知道數組的元素在內部是以行爲首要的順序存儲的。

    :dispaced-to關鍵字在將一個數組數據讀取成爲一個向量的時候是頗有用的:

> (make-array 6 :displaced-to x)
#(1 2 3 4 5 6)
例如,這能夠經過使用map函數來構建一個矩陣加法函數。

    多維數組的打印形式可使用讀取函數來讀入。那麼你能夠經過鍵入這個數組就像它打印成的樣子,用這樣的辦法來構建一個數組:

#2A((1 2) (3 4) (5 6))
> '#2a((1 2) (3 4) (5 6))
#2A((1 2) (3 4) (5 6))

這裏又須要引號了,由於對一個數組求值是沒有意義的。使用矢量讀取宏,當一個數組以這種方式構建的時候數組元素是不被求值的。

數組讀取

aref函數能夠用來提取數組的元素。使用上邊構建的數組x,咱們能夠提取第一行第零列的元素:

> x
#2A((1 2) (3 4) (5 6))
> (aref x 1 0)
3
setf函數可使用aref來修改一個數組的元素:
> (setf (aref x 1 0) 'a)
A
> x
#2A((1 2) (A 4) (5 6))
    Lisp-Stat函數select能夠用來讀取數組元素。select函數能夠像aref函數同樣使用:
> x
#2A((1 2) (A 4) (5 6))
> (select x 1 0)
A
> (setf (select x 1 0) 'b)
B
當select的下標參數是一個或者多個的時候, select函數也能夠傳遞給整數的列表或者向量,來提取一個子數組:

> (select x '(0 1) 1)
#2A((2) (4))
> (select x 1 '(0 1))
#2A((B 4))
> (select x '(0 1) '(0 1))
#2A((1 2) (B 4))
經過使用setf函數,並將select函數做爲通常變量,能夠修改子數組的值。
> (setf (select x '(0 1) 1) '#2A((x) (y)))
#2A((X) (Y))
> x
#2A((1 X) (B Y) (5 6))
在#2A((x) (y))裏的符號沒有被引用,這是由於它們被讀取的時候數組讀取宏沒有對數組參數求值。

    aref函數不容許索引列表,可是這在一些Lisp系統裏可能更加高效。

額外的數組信息

array-rank函數返回一個數組的階數:

> (array-rank '#(1 2 3))
1
> (array-rank '#2A((1 2 3) (4 5 6)))
2
> (array-rank (make-array '(2 3 4)))
3
array-dimension函數將返回指定維度的大小:
> (array-dimension '#(1 2 3) 0)
3
> (array-dimension '#2A((1 2 3) (4 5 6)) 0)
2
> (array-dimension '#2a((1 2 3) (4 5 6)) 1)
3
使用array-dimensions函數能夠獲取整個維度列表:
> (array-dimensions '#(1 2 3))
(3)
> (array-dimensions '#2a((1 2 3) (4 5 6)))
(2 3)
    array-total-size函數返回數組裏元素的數量,array-in-bounds-p肯定一個索引集合對一個數組是否合法(即給定的索引號是否在索引維度列表的範圍內):
> (array-total-size '#2a((1 2 3) (4 5 6)))
6
> (array-in-bounds-p '#2a((1 2 3) (4 5 6)) 2 1)
NIL
> (array-in-bounds-p '#2a((1 2 3) (4 5 6)) 1 2)
T
array-row-major-index函數帶一個數組和一個合法的索引集合做爲參數,返回指定元素在以行爲主要的順序存儲結構裏的位置。
> (array-row-major-index '#2a((1 2 3) (4 5 6)) 1 2)
5
下標爲1和2的元素是第二行的第三個元素,在 以行爲主要的順序存儲結構裏它是最後一個函數。那麼在以行爲主要的順序存儲結構裏它的下標爲5。

注:"以行爲主要的順序存儲結構",原文爲row-major ordering,即多維數組元素在內存裏的存儲順序,將多維的數組元素以一維的形式存放於內存之中。

4.4.8 其它的數據類型

除了在本節裏討論的數據類型以外,Lisp-Stat還有一個叫作object的數據類型,它用於面向對象編程。對象是第六章的主題。Common Lisp也有結構體這個數據類型,這與C的結構體和pascal的記錄數據類型是類似的。

4.5 什物

本節包含一些雜項主題集合,這些主題與前邊幾節不能很方便地切合。

4.5.1 錯誤

在計算過程當中發生一個錯誤的時候,Lisp將進入調試器,這個在4.5.3節裏描述,或者重啓系統以後返回到解釋器。Lisp提供一些工具用來從你的函數裏發送一個錯誤信號,這些工具還能夠用來確保執行一個清理步驟,即便錯誤發生在表達式求值階段。

發送錯誤信號

好比說一個內置函數的參數不是合適的類型,它將打印一個合理的帶有信息的錯誤消息。你可使用error函數讓你的函數打印這樣的消息。error函數帶有一個格式化字符參數,後邊還跟着一個格式化指令須要的額外的參數。錯誤字符串不該該包含指示錯誤已經發生的狀態,系統將提供這種指示,這裏有幾個例子:

> (error "An error has occurred")
Error: An error has occurred
Happened in: #<Subr-TOP-LEVEL-LOOP: #1372f1c>
> (error "Bad argment type type - ~a" 3)
Error: Bad argment type type - 3
Happened in: #<Subr-TOP-LEVEL-LOOP: #1372f1c>
解開保護

隨着出現錯誤的可能性程度,確保在錯誤發生以後系統重啓以前,採起必定的動做,這是很重要的。例如,若是在處理一個對話框窗體的動做時發生了一個錯誤,你可能想要確保這個對話框從屏幕移除。unwind-protect函數就是用來保證必定的清除動做發生的函數。調用一個unwind-protect函數的通常形式是:
(unwind-protect <protected expr>
                        <cleanup expr1 1>
                        ...
                        <cleanup expr n>)
unwind-protect對<protected expr>求值,而後對<cleanup expr1 1>, ...,<cleanup expr1 n>求值。即便在錯誤發生之後,在函數對<protected expr>表達式求值過程當中,發生了系統重啓,也會執行清楚表達式的。清除表達式自己不被進一步保護。unwind-protect表達式返回的結果就是<protected expr>表達式返回的結果。舉個例子,處理對話框而且保證及時發生錯誤對話框也能關閉的表達式能夠這樣編寫:(unwind-protect (do-dialog) (close-dialog))。

4.5.2 代碼編寫幫助

註釋,縮進和文檔

Lisp裏的註釋前邊要加一個分號。一行之中全部跟在一個分號以後的文本都會被Lisp讀取器忽略。依據慣例,不一樣數目的分號用在不一樣層次層級上:4個分號用於文件頭註釋,3個分號用於函數定義的註釋,2個分號用於函數內部開始部分的註釋,1個分號用於代碼的行註釋。

    註釋固然也常常能夠用來臨時性地移除某個代碼段。在每行前邊都加一個分號是件煩人的事兒,因此出現了多行註釋系統:全部出如今符號"#|"和"|#"之間的文本都將被忽略,不管有多少行。Lisp代碼,一般都帶有不少括號,若是不對它進行縮進處理以反映它的結構,它就會極難閱讀。好的Lisp編輯器,好比emacs編輯器,都會提供一個機制來根據必定的縮進慣例對函數進行自動縮進。Macintosh操做系統的XLISP-STAT解釋器和編輯器窗口也包含一個簡單的縮進機制:在一個代碼行上敲擊tab鍵,改行將縮進到合理的層次上。

    Lisp代碼在進行定義時包含一個機制即在代碼裏家入文檔。若是在一個由defun定義的函數體裏的第一個表達式是字符串的話,而且若是在函數體裏的表達式多於一個,那麼這個字符串將做爲函數符號的文檔安裝到函數內部,這裏的函數符號在defun定義的函數體裏做爲函數名。這些文檔字符串可使用documentation函數或者Lisp-Stat的help和help*函數獲取。例如,爲了提取函數符號mean的文檔,

> (documentation 'mean 'function)
loading in help file information - this will take a minute ...done
"Args: (x)
Returns the mean of the elements x. Vector reducing."
變量和類型文檔的字符串的提取也是類似的:
> (documentation 'pi 'variable)
"The floating-point number that is approximately equal to the ratio of the
circumference of a circle to its diameter."
> (documentation 'complex 'type)
"A complex number"
模塊

在將一個函數定義拆散並放到多個文件方面,函數require和provide是頗有用的。函數provide將它的參數,一個字符串加到*modules*這個列表中,前提是它不在該列表內。函數require檢查它的參數是否已經存在於*modules*列表。若是已經存在於該列表,就什麼也不作;不然,它將試圖加載它的參數。require也接受第二個參數用來做爲將被加載的文件的名字。若是沒有提供第二個參數,這個名字將從模塊的名字裏構造。

    爲了使用多個文件實現一個模塊系統,在文件起始部分放置一個provide表達式,後邊跟着一個require表達式,該require表達式引入該文件依賴的每個模塊。加載感興趣的主文件應該同時會引進其它文件,被require命令引入兩次及以上次數的文件只加載一次。

適應系統特徵

Common Lisp提供了一個機制,來探測系統是否有特別特性,還能夠在必定特徵或者必定特徵的組合的狀況下對錶達式求值。

    針對特定的系統,全局變量*features*包括一個特性列表。在帶色彩監視器的一個Macintosh系統裏的XLISP-STAT上,這個變量的值多是:

(注:各位,很差意思,沒在Mac上試驗過,現將原文拷貝如上圖,可是Windows下的XLISP-STAT裏的*features*是有的)

> *features*
(:COLOR :DIALOGS :WINDOWS :WIN32 :MSDOS :XLISP-STAT :XLISP-PLUS :XLISP)
    讀取宏#+和#-能夠分別用來對必定特徵組合存在或者不存在對錶達式進行求值。例如,若是給定一下讀取函數:#+color (add-color),那麼表達式(add-color)僅當符號color在features列表裏時纔會被求值。在#-color (fake-color)裏,表達式(fake-color)僅當符號color不在features列表裏時纔會被求值。

    跟在#+和#-後邊的符號也能夠替換成features裏的符號的邏輯表達式。例如:#+(and windows (not color)) (fake-color)表達式確保符號windows在features列表裏而且color符號不在features列表的時候,(fake-color)才被求值。

4.5.3 調試工具

在Lisp裏,三個調試工具是可用的:斷點、跟蹤機制和步進器。全部與Comon Lisp標準兼容的Lisp系統都至少提供了這三個機制,可是它們如何工做和它們產生的輸出因系統的不一樣而不一樣。本節我將描述在XLISP-STAT系統裏提供的這些機制的版本。

斷點

經過執行break函數,能夠輸入一個斷點。在一個函數代碼裏放置一個(break)表達式,就像這樣:
(defun f (x) (break) x)容許你在函數環境裏檢測變量。爲了從斷點處繼續運行,能夠執行continue函數:

> (defun f (x) (break) x)
F
> (f 3)
Break: **BREAK**
Break level 1.
To continue, type (continue n), where n is an option number:
 0: Return from BREAK.
 1: Return to Lisp Toplevel.
1> x
3
1> (continue)
3
提示符處的數字是斷點所處的循環的層級;若是在一個打斷的循環裏發生了另外一個打斷操做,該層級將遞增。break函數還接受一個可選的格式化字符串,它後邊跟着格式化指令所需的附加參數;這些都是用來在進入斷點處時能打印一個消息的。

    若是全局變量*breakenable*不爲nil,這時發生了一個錯誤,系統將自動進入斷點。若是*tracenable*也不是nil,一個調用的回溯引出的斷點將被打印出來。斷點的層級序號將由變量*tracelimit*控制。若是*tracenable*是nil的話,經過調用baktrace (sic) 函數你仍然能夠得到調用堆棧追溯。

    函數debug和nodebug提供了一個方便的方式來使*breakenable*在值nil和t之間來回切換。

    若是是由於發生一個錯誤而進入一個斷點,可能就沒法使用continue函數了。對於這種沒法繼續執行的錯誤,你能夠經過調用top-level函數來返回到頂層。在Macintosh版本的XLISP-STAT裏,你也能夠在Command菜單裏選擇Top-Level選項,或者是與它等價的命令鍵。

    下邊是一個斷點使能的例子:

> (defun f (x) (/ x 0))
F
> (debug)
T
> (f 3)
Error: illegal zero argument
Break level 1.
To continue, type (continue n), where n is an option number:
 0: Return to Lisp Toplevel.
1> (baktrace)
Function: #<Subr-/: #13df124>
Arguments:
  3
  0
Function: #<Closure-F: #140eaac>
Arguments:
  3
Function: #<Subr-TOP-LEVEL-LOOP: #13e2ac4>
1> (f 3)
Error: illegal zero argument
Break level 2.
To continue, type (continue n), where n is an option number:
 0: Return to break level 1.
 1: Return to Lisp Toplevel.
2> (clean-up)
NIL
2> (continue 0)
Break level 1.
To continue, type (continue n), where n is an option number:
 0: Return to Lisp Toplevel.
1> x
3
1> (top-level)
[ back to top level ]
跟蹤

爲了追蹤一個沒有使用斷點中斷的一個特定的函數的用途,你可使用trace函數跟蹤它。爲了跟蹤函數f,對下邊的表達式求值(trace f),爲了中止跟蹤,計算這個表達式(untrace f)。調用一個不帶參數的untrace函數將中止對當前全部的被跟蹤函數的跟蹤。在不須要引用參數的時候,trace和untrace函數就是宏t'。

    舉個例子,讓咱們來葛總一下下邊定義的一個遞歸函數factorial的執行過程吧:

> (defun factorial (n)
    (if (= n 0) 1
        (* (factorial (- n 1)) n)))
FACTORIAL
> (trace factorial)
(FACTORIAL)
> (factorial 6)
Entering: FACTORIAL, Argument list: (6)
 Entering: FACTORIAL, Argument list: (5)
  Entering: FACTORIAL, Argument list: (4)
   Entering: FACTORIAL, Argument list: (3)
    Entering: FACTORIAL, Argument list: (2)
     Entering: FACTORIAL, Argument list: (1)
      Entering: FACTORIAL, Argument list: (0)
      Exiting: FACTORIAL, Value: 1
     Exiting: FACTORIAL, Value: 1
    Exiting: FACTORIAL, Value: 2
   Exiting: FACTORIAL, Value: 6
  Exiting: FACTORIAL, Value: 24
 Exiting: FACTORIAL, Value: 120
Exiting: FACTORIAL, Value: 720
720
> (untrace factorial)
NIL
> (factorial 6)
720
步進器

步進器容許你每次只運行一個表達式求值。你能夠經過對下式求值來步進地對錶達式求值:(step expr)。每一步步進器都會等待一個應答,可能的應答以下:

  • :b - break
  • :h - help (this message)
  • :n - next
  • :s - skip
  • :e - evaluate

在Macintosh操做系統的XLISP-STAT裏,step函數將打開一個帶按鈕的對話框,用來構建這個應答。下邊是步進器會話的一個簡單的例子:

> (step (break '(+ 1 2 (* 3 (/ 2 4)))))


0 >==> (BREAK (QUOTE (+ 1 2 (* 3 (/ 2 4)))))
 1 >==> (QUOTE (+ 1 2 ...))  :n


 1 <==< (+ 1 2 (* 3 (/ 2 4)))Break: 
 1 >==> (XLISP::%STRUCT-REF X 1)  :n


           X = #<Condition SIMPLE-CONDITION: 1417be8>
 1 <==< (+ 1 2 (* 3 (/ 2 4)))
 1 >==> (XLISP::%STRUCT-REF X 2)  :n


           X = #<Condition SIMPLE-CONDITION: 1417be8>
 1 <==< NIL#<Condition SIMPLE-CONDITION: 1417be8>
Break level 1.
To continue, type (continue n), where n is an option number:
 0: Return from BREAK.
 1: Return to Lisp Toplevel.
1> (continue 0)


0 <==< NIL
NIL

4.5.4 定時器

爲了定時地計算求值和肯定系統時鐘的當前狀態,一些宏和函數是可用的。宏time帶一個表達式參數,對該表達式參數求值後,將打印本次求值須要的時間,最後返回表達式的結果:

> (time (mean (iseq 1 100000)))
The evaluation took 0.03 seconds; 0.03 seconds in gc.
50000.5
time的參數不須要被求值。

    函數get-internal-run-time和get-internal-real-time返回離起始點的總運行時間和總共過去的時間。時間起始點和量度單位是系統決定的。每秒鐘的內部時間單元的數量是全局變量internal-time-units-per-second的值。

4.5.5 Defsetf

使用setf能夠將一些讀取函數當作通常變量來操做。使用defsetf宏,你能夠定義setf方法來讀取你本身的函數。defsetf有好幾種用法,最簡答的形式是(defsetf <accessor> <access-fcn>)。參數<access-fcn>是一個命名函數的符號,它將傳遞給<accessor>的參數做爲本身的參數,這些參數後邊後跟着一個數值,經過使用<accessor>參數來進行位置定位。<access-fcn>應該返回參數的值做爲它的返回值。

    舉個例子,在3.10.2節中我介紹了功能抽象來表示加法的表達式。這個抽象包括一個構造函數make-sum和兩個讀取函數addend和augend。爲了可以改變一個加法操做裏的加數,咱們可使用這樣一個表達式(setf (addend mysum) 3),假設加法被表達出Lisp表達式,修改加數的函數能夠被定義成這樣:

> (defun set-addend (sum new)
    (setf (select sum 1) new)
    new)
SET-ADDEND
而後,經過下邊的表達式,這個函數能夠做爲addend的setf方法安裝起來:
> (defsetf addend set-addend)
ADDEND

如今,咱們可使用setf來改變一個加法式裏的加數:

> (defun make-sum (a1 a2) (list '+ a1 a2))
MAKE-SUM
> (defun addend (e) (second e))
ADDEND
> (defun augend (e) (third e))
AUGEND
> (setf s (make-sum 1 3))
(+ 1 3)
> (setf (addend s) 2)
2
> s
(+ 2 3)

4.5.6 特殊變量

默認地,Common Lisp變量是詞法做用域的。經過使用一個特殊的符號,使Lisp把變量看作成動態做用域變量,這是可能的。這樣的變量叫作特殊變量。不少預約義的系統常量就是這樣的特殊變量。

    特殊全局變量可使用defvar、defparameter和defconstant來設置。defvar設置一個全局變量並將它初始化爲nil或者經過第二個可選參數來指定初始化值,除非它已經有一個值了。若是該變量已經有一個值,該值將不會改變,而且參數列表提供的賦值表達式也不會被求值。defvar也接收一個字符串做爲第三個參數,它將被安裝到變量裏做爲變量的文檔字符串。可使用documentation函數或者help或者help*函數來取得該字符串:

> (defvar x (list 1 2 3) "A simple data set")
X
    defparameter與defvar類似,不一樣的是它須要一個值,而且一般將該值賦給它定義的變量,不管這個變量是否已經有值。defconstant與defparameter類似,不一樣的是它將變量標記爲常變量,它是不能被修改的:
> (defconstant e (exp 1) "Base of the natural logarithm")
E
> (setf e 3)
Error: can't assign/bind to constant - E
Happened in: #<FSubr-SETQ: #13c4b50>
變量pi就是常量,t是一個值爲t的常量,每一個關鍵字(即以冒號開始的符號)就是值爲其自身的常量。
相關文章
相關標籤/搜索