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

给你一囗甜甜゛ 提交于 2019-12-06 21:23:59

第四章 其它Lisp特性

    上一章介绍了Lisp编程的基础,在那章里重点展示了对编写Lisp函数有用的编程技术。为了最高效地使用这些技术,知道Lisp和Lisp-Stat提供的函数和数据类型将是很有用的。本章旨在给出这些函数和数据类型的概貌。初次阅读,你可以略读,当你需要的时候再将其作为参考手册来使用。

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。

读取器还负责以一个特殊的方式解释 一些字符,或者字符序列。这样一个字符序列和用来解释它的代码,叫做一个读取宏。我们已经使用过一些读取宏:引号字符"'",反引号字符"`",逗号字符",",和一对字符"#'"。为了理解他们是如何工作的,我们可以将包含这些读取宏的引号表达式输入到解释器,然后观察返回了什么:

> (quote 'x)
(QUOTE X)
> (quote `x)
(BACKQUOTE X)
> (quote ,x)
(COMMA X)
> (quote #'x)
(FUNCTION X)
例如,当读取器看到一个引号字符"'"的时候,它将读取下一个Lisp表达式,然后返回包含符号quot和表达式的一个列表。

    其它读取宏还有双引号字符",他将读取紧跟着它的字符直到遇到下一个"为止,并将它们形成一个字符串,反斜线字符\,它是针对与接下来有关联的字符,用来转义任何特殊意思的字符。例如,一个反斜线可以用来产生一个包含双引号的字符串。下面我们将会遇到一些其它的读取宏。

    定义自己的读取宏是可能的,通过这个方法,你可以自定义Lisp读取器来用你喜欢的语言语法来提供一个词法分析器。

4.1.2 基础打印函数

    为了向标准输出流或者一个文件里打印数据,一些函数是可用的。最基本的函数是print、prin1、princ和terpri。print和prin都会打印它们的参数给一个form,这个form对Lisp读取器来说是可读的。如果可以的话,读取器应该能够读取由这些函数打印的东西。print和prin1之间的不同是print的输出的前边加了一个空行,后边跟了一个空格。princ和prin1相似,除了princ忽略了所有的转义字符。特别地,princ打印的字符串不带双引号。函数terpri输出一个新行。

    为了展示这些函数,让我们看一下下边定义的函数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包含格式化指令,它以一个波浪线开头,这些位置将被剩下的参数填满。

    最简单的格式化指令是~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、1、2、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的常量,每个关键字(即以冒号开始的符号)就是值为其自身的常量。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!