上一章介绍了Lisp编程的基础,在那章里重点展现了对编写Lisp函数有用的编程技术。为了最高效地使用这些技术,知道Lisp和Lisp-Stat提供的函数和数据类型将是颇有用的。本章旨在给出这些函数和数据类型的概貌。初次阅读,你能够略读,当你须要的时候再将其做为参考手册来使用。 node
Common Lisp编程语言包含一个可扩展的工具集,能够用来读写文件、格式化输出和自定义读取输入的方式。这些特征的完整讨论将占用几章内容,所以我只展现那些我发现的对统计编程有用的方面。更多的可扩展话题能够到一些Common 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
为了向标准输出流或者一个文件里打印数据,一些函数是可用的。最基本的函数是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函数的返回值。
若是你须要更多的输出控制,你可使用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
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) HELLOread函数每次从指定流里读取一个表达式。
若是流是空的,或者已经到达文件尾,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)) NILwith-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>表达式求值,在全部的表达式被处理完后返回包含在流里的字符串:
目前,咱们遇到的一些内建函数或者是带可选参数的,或者是带关键字参数的,或者容许使用任意数量的参数。在你自定义的函数里也能够包括这些特性。
经过在参数列表里放置一个&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*结构,这个初始化过程能够认为是隐式的。
函数能够带多个关键字参数,关键字参数能够以任意顺序被使用,只须要跟在须要的参数后边。
在咱们的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))) FACTORIALsupplied-p参数也能够像关键字参数里那样工做。
与关键字参数不一样的是,若是一个函数带多于一个的关键字参数,那么这些可选参数必须以他们在函数定义里指定的顺序使用。若是一个函数里既使用了可选参数也是用了关键字参数,那么在函数定义时可选参数必需要在关键字参数前边。在使用同时带可选参数和关键字参数的函数时,若是你想使用任何关键字参数,必须指定全部的可选参数。
在第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的顺序出现。
咱们已经普遍使用了条件求值结构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>))是分别等价的。
目前为止,可用的最灵活的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表达式而结束的,那么标准返回表达式将不被求值。
到目前为止,咱们普遍使用的Lisp数据类型仅是数值、字符串、符号和列表。本节将复习这些数据类型而后介绍一些新的数据类型,尤为是矢量和多维数组。
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里,这些函数都是矢量化的。
咱们已经在不少例子里使用过字符串了,它们被写成包含在一对双引号内部的字符序列。字符是组成字符串的元素。他们(字符串)被当作是一种特殊的数据类型,与单元素字符串和数值型字符代码相区别。为了弄清楚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两个谓词来识别。
与其它大多数语言不一样,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.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)))) Tappend函数所作的重用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。
目前为止,咱们已经将数值型数据集表示为列表形式,咱们也能够将它们表示成向量的形式。向量可使用相似下边的表达式来构建:
> '#(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系统里列表在使用内存方面也能够比向量更加高效。
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)) NILposition函数与find函数的参数列表相同,它返回与参数列表的第一个参数相匹配的那个元素的下标,若是没有匹配值则返回nil:
> (position 'b '(a b c)) 1 > (position 'd '(a b c)) NILfind函数和position函数都是用eql做为它们默认的谓词测试函数,若是想使用备用的测试函数,能够用:test关键字来指定。
Common Lisp提供了一个强大的排序函数sort函数。sort函数须要两个参数,一个序列和一个谓词。谓词应该是一个函数,该函数带两个参数,而且仅当第一个参数严格小于第二个参数的时候返回非nil结果。sort函数应该谨慎使用,由于它会破坏性地对序列进行排序。也就是说,因为效率的缘由,在运行排序算法的时候,它将部分地重用参数的数据,在处理过程当中破坏了原来的序列。那么若是你想保护参数,在你将它传递给sort函数以前要进行数据拷贝。相反地,第2.7.3节里介绍的sort-data函数是非破坏性的函数。
向量是一维的数值。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) 3setf函数可使用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))) 3array-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) Tarray-row-major-index函数带一个数组和一个合法的索引集合做为参数,返回指定元素在以行为主要的顺序存储结构里的位置。
> (array-row-major-index '#2a((1 2 3) (4 5 6)) 1 2) 5下标为1和2的元素是第二行的第三个元素,在 以行为主要的顺序存储结构里它是最后一个函数。那么在以行为主要的顺序存储结构里它的下标为5。
注:"以行为主要的顺序存储结构",原文为row-major ordering,即多维数组元素在内存里的存储顺序,将多维的数组元素以一维的形式存放于内存之中。
除了在本节里讨论的数据类型以外,Lisp-Stat还有一个叫作object的数据类型,它用于面向对象编程。对象是第六章的主题。Common Lisp也有结构体这个数据类型,这与C的结构体和pascal的记录数据类型是类似的。
本节包含一些杂项主题集合,这些主题与前边几节不能很方便地切合。
在计算过程当中发生一个错误的时候,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))。
注释,缩进和文档
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)才被求值。
在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)。每一步步进器都会等待一个应答,可能的应答以下:
在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
为了定时地计算求值和肯定系统时钟的当前状态,一些宏和函数是可用的。宏time带一个表达式参数,对该表达式参数求值后,将打印本次求值须要的时间,最后返回表达式的结果:
> (time (mean (iseq 1 100000))) The evaluation took 0.03 seconds; 0.03 seconds in gc. 50000.5time的参数不须要被求值。
函数get-internal-run-time和get-internal-real-time返回离起始点的总运行时间和总共过去的时间。时间起始点和量度单位是系统决定的。每秒钟的内部时间单元的数量是全局变量internal-time-units-per-second的值。
使用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)
默认地,Common Lisp变量是词法做用域的。经过使用一个特殊的符号,使Lisp把变量看作成动态做用域变量,这是可能的。这样的变量叫作特殊变量。不少预约义的系统常量就是这样的特殊变量。
特殊全局变量可使用defvar、defparameter和defconstant来设置。defvar设置一个全局变量并将它初始化为nil或者经过第二个可选参数来指定初始化值,除非它已经有一个值了。若是该变量已经有一个值,该值将不会改变,而且参数列表提供的赋值表达式也不会被求值。defvar也接收一个字符串做为第三个参数,它将被安装到变量里做为变量的文档字符串。可使用documentation函数或者help或者help*函数来取得该字符串:
> (defvar x (list 1 2 3) "A simple data set") Xdefparameter与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的常量,每一个关键字(即以冒号开始的符号)就是值为其自身的常量。