<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;tid=571&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / ANSI Common Lisp  第五章：控制流]]></title>
		<link>http://www.gentoo-zh.org/viewtopic.php?id=571</link>
		<description><![CDATA[ANSI Common Lisp  第五章：控制流 最近发表的帖子。]]></description>
		<lastBuildDate>Fri, 18 Nov 2022 13:08:10 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[ANSI Common Lisp  第五章：控制流]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?pid=611#p611</link>
			<description><![CDATA[<p>2.2 节介绍过 Common Lisp 的求值规则，现在你应该很熟悉了。本章的操作符都有一个共同点，就是它们都违反了求值规则。这些操作符让你决定在程序当中何时要求值。如果普通的函数调用是 Lisp 程序的树叶的话，那这些操作符就是连结树叶的树枝。</p><p>&#160; &#160; &#160; &#160; 5.1 区块 (Blocks)<br />&#160; &#160; &#160; &#160; 5.2 语境 (Context)<br />&#160; &#160; &#160; &#160; 5.3 条件 (Conditionals)<br />&#160; &#160; &#160; &#160; 5.4 迭代 (Iteration)<br />&#160; &#160; &#160; &#160; 5.5 多值 (Multiple Values)<br />&#160; &#160; &#160; &#160; 5.6 中止 (Aborts)<br />&#160; &#160; &#160; &#160; 5.7 示例：日期运算 (Example: Date Arithmetic)<br />&#160; &#160; &#160; &#160; Chapter 5 总结 (Summary)<br />&#160; &#160; &#160; &#160; Chapter 5 练习 (Exercises)</p><p>5.1 区块 (Blocks)</p><p>Common Lisp 有三个构造区块（block）的基本操作符： progn 、 block 以及 tagbody 。我们已经看过 progn 了。在 progn 主体中的表达式会依序求值，并返回最后一个表达式的值：</p><p>&gt; (progn<br />&#160; &#160; (format t &quot;a&quot;)<br />&#160; &#160; (format t &quot;b&quot;)<br />&#160; &#160; (+ 11 12))<br />ab<br />23</p><p>由于只返回最后一个表达式的值，代表著使用 progn （或任何区块）涵盖了副作用。</p><p>一个 block 像是带有名字及紧急出口的 progn 。第一个实参应为符号。这成为了区块的名字。在主体中的任何地方，可以停止求值，并通过使用 return-from 指定区块的名字，来立即返回数值：</p><p>&gt; (block head<br />&#160; &#160; (format t &quot;Here we go.&quot;)<br />&#160; &#160; (return-from head &#039;idea)<br />&#160; &#160; (format t &quot;We&#039;ll never see this.&quot;))<br />Here we go.<br />IDEA</p><p>调用 return-from 允许你的程序，从代码的任何地方，突然但优雅地退出。第二个传给 return-from 的实参，用来作为以第一个实参为名的区块的返回值。在 return-from 之后的表达式不会被求值。</p><p>也有一个 return 宏，它把传入的参数当做封闭区块 nil 的返回值：</p><p>&gt; (block nil<br />&#160; &#160; (return 27))<br />27</p><p>许多接受一个表达式主体的 Common Lisp 操作符，皆隐含在一个叫做 nil 的区块里。比如，所有由 do 构造的迭代函数：</p><p>&gt; (dolist (x &#039;(a b c d e))<br />&#160; &#160; (format t &quot;~A &quot; x)<br />&#160; &#160; (if (eql x &#039;c)<br />&#160; &#160; &#160; &#160; (return &#039;done)))<br />A B C<br />DONE</p><p>使用 defun 定义的函数主体，都隐含在一个与函数同名的区块，所以你可以：</p><p>(defun foo ()<br />&#160; (return-from foo 27))</p><p>在一个显式或隐式的 block 外，不论是 return-from 或 return 都不会工作。</p><p>使用 return-from ，我们可以写出一个更好的 read-integer 版本：</p><p>(defun read-integer (str)<br />&#160; (let ((accum 0))<br />&#160; &#160; (dotimes (pos (length str))<br />&#160; &#160; &#160; (let ((i (digit-char-p (char str pos))))<br />&#160; &#160; &#160; &#160; (if i<br />&#160; &#160; &#160; &#160; &#160; &#160; (setf accum (+ (* accum 10) i))<br />&#160; &#160; &#160; &#160; &#160; &#160; (return-from read-integer nil))))<br />&#160; &#160; accum))</p><p>68 页的版本在构造整数之前，需检查所有的字符。现在两个步骤可以结合，因为如果遇到非数字的字符时，我们可以舍弃计算结果。出现在主体的原子（atom）被解读为标签（labels)；把这样的标签传给 go ，会把控制权交给标签后的表达式。以下是一个非常丑的程序片段，用来印出一至十的数字：</p><p>&gt; (tagbody<br />&#160; &#160; (setf x 0)<br />&#160; &#160; top<br />&#160; &#160; &#160; (setf x (+ x 1))<br />&#160; &#160; &#160; (format t &quot;~A &quot; x)<br />&#160; &#160; &#160; (if (&lt; x 10) (go top)))<br />1 2 3 4 5 6 7 8 9 10<br />NIL</p><p>这个操作符主要用来实现其它的操作符，不是一般会用到的操作符。大多数迭代操作符都隐含在一个 tagbody ，所以是可能可以在主体里（虽然很少想要）使用标签及 go 。</p><p>如何决定要使用哪一种区块建构子呢（block construct）？几乎任何时候，你会使用 progn 。如果你想要突然退出的话，使用block 来取代。多数程序员永远不会显式地使用 tagbody 。<br />5.2 语境 (Context)</p><p>另一个我们用来区分表达式的操作符是 let 。它接受一个代码主体，但允许我们在主体内设置新变量：</p><p>&gt; (let ((x 7)<br />&#160; &#160; &#160; &#160; (y 2))<br />&#160; &#160; (format t &quot;Number&quot;)<br />&#160; &#160; (+ x y))<br />Number<br />9</p><p>一个像是 let 的操作符，创造出一个新的词法语境（lexical context）。在这个语境里有两个新变量，然而在外部语境的变量也因此变得不可视了。</p><p>概念上说，一个 let 表达式等同于函数调用。在 2.14 节证明过，函数可以用名字来引用，也可以通过使用一个 lambda 表达式从字面上来引用。由于 lambda 表达式是函数的名字，我们可以像使用函数名那样，把 lambda 表达式作为函数调用的第一个实参：</p><p>&gt; ((lambda (x) (+ x 1)) 3)<br />4</p><p>前述的 let 表达式，实际上等同于：</p><p>((lambda (x y)<br />&#160; &#160;(format t &quot;Number&quot;)<br />&#160; &#160;(+ x y))<br /> 7<br /> 2)</p><p>如果有关于 let 的任何问题，应该是如何把责任交给 lambda ，因为进入一个 let 等同于执行一个函数调用。</p><p>这个模型清楚的告诉我们，由 let 创造的变量的值，不能依赖其它由同一个 let 所创造的变量。举例来说，如果我们试着：</p><p>(let ((x 2)<br />&#160; &#160; &#160; (y (+ x 1)))<br />&#160; (+ x y))</p><p>在 (+ x 1) 中的 x 不是前一行所设置的值，因为整个表达式等同于：</p><p>((lambda (x y) (+ x y)) 2<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (+ x 1))</p><p>这里明显看到 (+ x 1) 作为实参传给函数，不能引用函数内的形参 x 。</p><p>所以如果你真的想要新变量的值，依赖同一个表达式所设立的另一个变量？在这个情况下，使用一个变形版本 let* ：</p><p>&gt; (let* ((x 1)<br />&#160; &#160; &#160; &#160; &#160;(y (+ x 1)))<br />&#160; &#160; (+ x y))<br />3</p><p>一个 let* 功能上等同于一系列嵌套的 let 。这个特别的例子等同于：</p><p>(let ((x 1))<br />&#160; (let ((y (+ x 1)))<br />&#160; &#160; (+ x y)))</p><p>let 与 let* 将变量初始值都设为 nil 。nil 为初始值的变量，不需要依附在列表内:</p><p>&gt; (let (x y)<br />&#160; &#160; (list x y))<br />(NIL NIL)</p><p>destructuring-bind 宏是通用化的 let 。其接受单一变量，一个模式 (pattern) ── 一个或多个变量所构成的树 ── 并将它们与某个实际的树所对应的部份做绑定。举例来说：</p><p>&gt; (destructuring-bind (w (x y) . z) &#039;(a (b c) d e)<br />&#160; &#160; (list w x y z))<br />(A B C (D E))</p><p>若给定的树（第二个实参）没有与模式匹配（第一个参数）时，会产生错误。<br />5.3 条件 (Conditionals)</p><p>最简单的条件式是 if ；其余的条件式都是基于 if 所构造的。第二简单的条件式是 when ，它接受一个测试表达式（test expression）与一个代码主体。若测试表达式求值返回真时，则对主体求值。所以</p><p>(when (oddp that)<br />&#160; (format t &quot;Hmm, that&#039;s odd.&quot;)<br />&#160; (+ that 1))</p><p>等同于</p><p>(if (oddp that)<br />&#160; &#160; (progn<br />&#160; &#160; &#160; (format t &quot;Hmm, that&#039;s odd.&quot;)<br />&#160; &#160; &#160; (+ that 1)))</p><p>when 的相反是 unless ；它接受相同的实参，但仅在测试表达式返回假时，才对主体求值。</p><p>所有条件式的母体 (从正反两面看) 是 cond ， cond 有两个新的优点：允许多个条件判断，与每个条件相关的代码隐含在 progn 里。cond 预期在我们需要使用嵌套 if 的情况下使用。 举例来说，这个伪 member 函数</p><p>(defun our-member (obj lst)<br />&#160; (if (atom lst)<br />&#160; &#160; &#160; nil<br />&#160; &#160; &#160; (if (eql (car lst) obj)<br />&#160; &#160; &#160; &#160; &#160; lst<br />&#160; &#160; &#160; &#160; &#160; (our-member obj (cdr lst)))))</p><p>也可以定义成：</p><p>(defun our-member (obj lst)<br />&#160; (cond ((atom lst) nil)<br />&#160; &#160; &#160; &#160; ((eql (car lst) obj) lst)<br />&#160; &#160; &#160; &#160; (t (our-member obj (cdr lst)))))</p><p>事实上，Common Lisp 实现大概会把 cond 翻译成 if 的形式。</p><p>总得来说呢， cond 接受零个或多个实参。每一个实参必须是一个具有条件式，伴随着零个或多个表达式的列表。当 cond 表达式被求值时，测试条件式依序求值，直到某个测试条件式返回真才停止。当返回真时，与其相关联的表达式会被依序求值，而最后一个返回的数值，会作为 cond 的返回值。如果符合的条件式之后没有表达式的话：</p><p>&gt; (cond (99))<br />99</p><p>则会返回条件式的值。</p><p>由于 cond 子句的 t 条件永远成立，通常我们把它放在最后，作为缺省的条件式。如果没有子句符合时，则 cond 返回 nil ，但利用nil 作为返回值是一种很差的风格 (这种问题可能发生的例子，请看 292 页)。译注: Appendix A, unexpected nil 小节。</p><p>当你想要把一个数值与一系列的常量比较时，有 case 可以用。我们可以使用 case 来定义一个函数，返回每个月份中的天数：</p><p>(defun month-length (mon)<br />&#160; (case mon<br />&#160; &#160; ((jan mar may jul aug oct dec) 31)<br />&#160; &#160; ((apr jun sept nov) 30)<br />&#160; &#160; (feb (if (leap-year) 29 28))<br />&#160; &#160; (otherwise &quot;unknown month&quot;)))</p><p>一个 case 表达式由一个实参开始，此实参会被拿来与每个子句的键值做比较。接着是零个或多个子句，每个子句由一个或一串键值开始，跟随着零个或多个表达式。键值被视为常量；它们不会被求值。第一个参数的值被拿来与子句中的键值做比较 (使用 eql )。如果匹配时，子句剩余的表达式会被求值，并将最后一个求值作为 case 的返回值。</p><p>缺省子句的键值可以是 t 或 otherwise 。如果没有子句符合时，或是子句只包含键值时，</p><p>&gt; (case 99 (99))<br />NIL</p><p>则 case 返回 nil 。</p><p>typecase 宏与 case 相似，除了每个子句中的键值应为类型修饰符 (type specifiers)，以及第一个实参与键值比较的函数使用typep 而不是 eql (一个 typecase 的例子在 107 页)。 译注: 6.5 小节。<br />5.4 迭代 (Iteration)</p><p>最基本的迭代操作符是 do ，在 2.13 小节介绍过。由于 do 包含了隐式的 block 及 tagbody ，我们现在知道是可以在 do 主体内使用 return 、 return-from 以及 go 。</p><p>2.13 节提到 do 的第一个参数必须是说明变量规格的列表，列表可以是如下形式：</p><p>(variable&#160; initial&#160; update)</p><p>initial 与 update 形式是选择性的。若 update 形式忽略时，每次迭代时不会更新变量。若 initial 形式也忽略时，变量会使用nil 来初始化。</p><p>在 23 页的例子中（译注: 2.13 节），</p><p>(defun show-squares (start end)<br />&#160; (do ((i start (+ i 1)))<br />&#160; &#160; &#160; ((&gt; i end) &#039;done)<br />&#160; &#160; (format t &quot;~A ~A~%&quot; i (* i i))))</p><p>update 形式引用到由 do 所创造的变量。一般都是这么用。如果一个 do 的 update 形式，没有至少引用到一个 do 创建的变量时，反而很奇怪。</p><p>当同时更新超过一个变量时，问题来了，如果一个 update 形式，引用到一个拥有自己的 update 形式的变量时，它会被更新呢？或是获得前一次迭代的值？使用 do 的话，它获得后者的值：</p><p>&gt; (let ((x &#039;a))<br />&#160; &#160; (do ((x 1 (+ x 1))<br />&#160; &#160; &#160; &#160; &#160;(y x x))<br />&#160; &#160; &#160; &#160; ((&gt; x 5))<br />&#160; &#160; &#160; (format t &quot;(~A ~A)&#160; &quot; x y)))<br />(1 A)&#160; (2 1)&#160; (3 2)&#160; (4 3)&#160; (5 4)<br />NIL</p><p>每一次迭代时， x 获得先前的值，加上一； y 也获得 x 的前一次数值。</p><p>但也有一个 do* ，它有着和 let 与 let* 一样的关系。任何 initial 或 update 形式可以参照到前一个子句的变量，并会获得当下的值：</p><p>&gt; (do* ((x 1 (+ x 1))<br />&#160; &#160; &#160; (y x x))<br />&#160; &#160; &#160;((&gt; x 5))<br />&#160; (format t &quot;(~A ~A) &quot; x y))<br />(1 1) (2 2) (3 3) (4 4) (5 5)<br />NIL</p><p>除了 do 与 do* 之外，也有几个特别用途的迭代操作符。要迭代一个列表的元素，我们可以使用 dolist :</p><p>&gt; (dolist (x &#039;(a b c d) &#039;done)<br />&#160; &#160; (format t &quot;~A &quot; x))<br />A B C D<br />DONE</p><p>当迭代结束时，初始列表内的第三个表达式 (译注: done ) ，会被求值并作为 dolist 的返回值。缺省是 nil 。</p><p>有着同样的精神的是 dotimes ，给定某个 n ，将会从整数 0 ，迭代至 n-1 :</p><p>(dotimes (x 5 x)<br />&#160; (format t &quot;~A &quot; x))<br />0 1 2 3 4<br />5</p><p>dolist 与 dotimes 初始列表的第三个表达式皆可省略，省略时为 ``nil 。注意该表达式可引用到迭代过程中的变量。</p><p>（译注：第三个表达式即上例之 x ，可以省略，省略时 dotimes 表达式的返回值为 nil 。）</p><p>do 的重点 (THE POINT OF do)</p><p>在 “The Evolution of Lisp” 里，Steele 与 Garbriel 陈述了 do 的重点， 表达的实在太好了，值得整个在这里引用过来：</p><p>撇开争论语法不谈，有件事要说明的是，在任何一个编程语言中，一个循环若一次只能更新一个变量是毫无用处的。 几乎在任何情况下，会有一个变量用来产生下个值，而另一个变量用来累积结果。如果循环语法只能产生变量， 那么累积结果就得借由赋值语句来“手动”实现…或有其他的副作用。具有多变量的 do 循环，体现了产生与累积的本质对称性，允许可以无副作用地表达迭代过程：</p><p>(defun factorial (n)<br />&#160; (do ((j n (- j 1))<br />&#160; &#160; &#160; &#160;(f 1 (* j f)))<br />&#160; &#160; ((= j 0) f)))</p><p>当然在 step 形式里实现所有的实际工作，一个没有主体的 do 循环形式是较不寻常的。</p><p>函数 mapc 和 mapcar 很像，但不会 cons 一个新列表作为返回值，所以使用的唯一理由是为了副作用。它们比 dolist 来得灵活，因为可以同时遍历多个列表：</p><p>&gt; (mapc #&#039;(lambda (x y)<br />&#160; &#160; &#160; &#160; &#160; (format t &quot;~A ~A&#160; &quot; x y))<br />&#160; &#160; &#160; &#039;(hip flip slip)<br />&#160; &#160; &#160; &#039;(hop flop slop))<br />HIP HOP&#160; FLIP FLOP&#160; SLIP SLOP<br />(HIP FLIP SLIP)</p><p>总是返回 mapc 的第二个参数。<br />5.5 多值 (Multiple Values)</p><p>曾有人这么说，为了要强调函数式编程的重要性，每个 Lisp 表达式都返回一个值。现在事情不是这么简单了；在 Common Lisp 里，一个表达式可以返回零个或多个数值。最多可以返回几个值取决于各家实现，但至少可以返回 19 个值。</p><p>多值允许一个函数返回多件事情的计算结果，而不用构造一个特定的结构。举例来说，内置的 get-decoded-time 返回 9 个数值来表示现在的时间：秒，分，时，日期，月，年，天，以及另外两个数值。</p><p>多值也使得查询函数可以分辨出 nil 与查询失败的情况。这也是为什么 gethash 返回两个值。因为它使用第二个数值来指出成功还是失败，我们可以在哈希表里储存 nil ，就像我们可以储存别的数值那样。</p><p>values 函数返回多个数值。它一个不少地返回你作为数值所传入的实参：</p><p>&gt; (values &#039;a nil (+ 2 4))<br />A<br />NIL<br />6</p><p>如果一个 values 表达式，是函数主体最后求值的表达式，它所返回的数值变成函数的返回值。多值可以原封不地通过任何数量的返回来传递：</p><p>&gt; ((lambda () ((lambda () (values 1 2)))))<br />1<br />2</p><p>然而若只预期一个返回值时，第一个之外的值会被舍弃：</p><p>&gt; (let ((x (values 1 2)))<br />&#160; &#160; x)<br />1</p><p>通过不带实参使用 values ，是可能不返回值的。在这个情况下，预期一个返回值的话，会获得 nil :</p><p>&gt; (values)<br />&gt; (let ((x (values)))<br />&#160; &#160; x)<br />NIL</p><p>要接收多个数值，我们使用 multiple-value-bind :</p><p>&gt; (multiple-value-bind (x y z) (values 1 2 3)<br />&#160; &#160; (list x y z))<br />(1 2 3)</p><p>&gt; (multiple-value-bind (x y z) (values 1 2)<br />&#160; &#160; (list x y z))<br />(1 2 NIL)</p><p>如果变量的数量大于数值的数量，剩余的变量会是 nil 。如果数值的数量大于变量的数量，多余的值会被舍弃。所以只想印出时间我们可以这么写:</p><p>&gt; (multiple-value-bind (s m h) (get-decoded-time)<br />&#160; &#160; (format t &quot;~A:~A:~A&quot; h m s))<br />&quot;4:32:13&quot;</p><p>你可以借由 multiple-value-call 将多值作为实参传给第二个函数：</p><p>&gt; (multiple-value-call #&#039;+ (values 1 2 3))<br />6</p><p>还有一个函数是 multiple-value-list :</p><p>&gt; (multiple-value-list (values &#039;a &#039;b &#039;c))<br />(A B C)</p><p>看起来像是使用 #&#039;list 作为第一个参数的来调用 multiple-value-call 。<br />5.6 中止 (Aborts)</p><p>你可以使用 return 在任何时候离开一个 block 。有时候我们想要做更极端的事，在数个函数调用里将控制权转移回来。要达成这件事，我们使用 catch 与 throw 。一个 catch 表达式接受一个标签（tag），标签可以是任何类型的对象，伴随着一个表达式主体：</p><p>(defun super ()<br />&#160; (catch &#039;abort<br />&#160; &#160; (sub)<br />&#160; &#160; (format t &quot;We&#039;ll never see this.&quot;)))</p><p>(defun sub ()<br />&#160; (throw &#039;abort 99))</p><p>表达式依序求值，就像它们是在 progn 里一样。在这段代码里的任何地方，一个带有特定标签的 throw 会导致 catch 表达式直接返回：</p><p>&gt; (super)<br />99</p><p>一个带有给定标签的 throw ，为了要到达匹配标签的 catch ，会将控制权转移 (因此杀掉进程)给任何有标签的 catch 。如果没有一个 catch 符合欲匹配的标签时， throw 会产生一个错误。</p><p>调用 error 同时中断了执行，本来会将控制权转移到调用树（calling tree）的更高点，取而代之的是，它将控制权转移给 Lisp 错误处理器（error handler）。通常会导致调用一个中断循环（break loop）。以下是一个假定的 Common Lisp 实现可能会发生的事情：</p><p>&gt; (progn<br />&#160; &#160; (error &quot;Oops!&quot;)<br />&#160; &#160; (format t &quot;After the error.&quot;))<br />Error: Oops!<br />&#160; &#160; &#160; &#160;Options: :abort, :backtrace<br />&gt;&gt;</p><p>译注：2 个 &gt;&gt; 显示进入中断循环了。</p><p>关于错误与状态的更多讯息，参见 14.6 小节以及附录 A。</p><p>有时候你想要防止代码被 throw 与 error 打断。借由使用 unwind-protect ，可以确保像是前述的中断，不会让你的程序停在不一致的状态。一个 unwind-protect 接受任何数量的实参，并返回第一个实参的值。然而即便是第一个实参的求值被打断时，剩下的表达式仍会被求值：</p><p>&gt; (setf x 1)<br />1<br />&gt; (catch &#039;abort<br />&#160; &#160; (unwind-protect<br />&#160; &#160; &#160; (throw &#039;abort 99)<br />&#160; &#160; &#160; (setf x 2)))<br />99<br />&gt; x<br />2</p><p>在这里，即便 throw 将控制权交回监测的 catch ， unwind-protect 确保控制权移交时，第二个表达式有被求值。无论何时，一个确切的动作要伴随着某种清理或重置时， unwind-protect 可能会派上用场。在 121 页提到了一个例子。<br />5.7 示例：日期运算 (Example: Date Arithmetic)</p><p>在某些应用里，能够做日期的加减是很有用的 ── 举例来说，能够算出从 1997 年 12 月 17 日，六十天之后是 1998 年 2 月 15 日。在这个小节里，我们会编写一个实用的工具来做日期运算。我们会将日期转成整数，起始点设置在 2000 年 1 月 1 日。我们会使用内置的 + 与 - 函数来处理这些数字，而当我们转换完毕时，再将结果转回日期。</p><p>要将日期转成数字，我们需要从日期的单位中，算出总天数有多少。举例来说，2004 年 11 月 13 日的天数总和，是从起始点至 2004 年有多少天，加上从 2004 年到 2004 年 11 月有多少天，再加上 13 天。</p><p>有一个我们会需要的东西是，一张列出非润年每月份有多少天的表格。我们可以使用 Lisp 来推敲出这个表格的内容。我们从列出每月份的长度开始：</p><p>&gt; (setf mon &#039;(31 28 31 30 31 30 31 31 30 31 30 31))<br />(31 28 31 30 31 30 31 31 30 31 30 31)</p><p>我们可以通过应用 + 函数至这个列表来测试总长度：</p><p>&gt; (apply #&#039;+ mon)<br />365</p><p>现在如果我们反转这个列表并使用 maplist 来应用 + 函数至每下一个 cdr 上，我们可以获得从每个月份开始所累积的天数：</p><p>&gt; (setf nom (reverse mon))<br />(31 30 31 30 31 31 30 31 30 31 28 31)<br />&gt; (setf sums (maplist #&#039;(lambda (x)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (apply #&#039;+ x))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; nom))<br />(365 334 304 273 243 212 181 151 120 90 59 31)</p><p>这些数字体现了从二月一号开始已经过了 31 天，从三月一号开始已经过了 59 天……等等。</p><p>我们刚刚建立的这个列表，可以转换成一个向量，见图 5.1，转换日期至整数的代码。</p><p>(defconstant month<br />&#160; #(0 31 59 90 120 151 181 212 243 273 304 334 365))</p><p>(defconstant yzero 2000)</p><p>(defun leap? (y)<br />&#160; (and (zerop (mod y 4))<br />&#160; &#160; &#160; &#160;(or (zerop (mod y 400))<br />&#160; &#160; &#160; &#160; &#160; &#160;(not (zerop (mod y 100))))))</p><p>(defun date-&gt;num (d m y)<br />&#160; (+ (- d 1) (month-num m y) (year-num y)))</p><p>(defun month-num (m y)<br />&#160; (+ (svref month (- m 1))<br />&#160; &#160; &#160;(if (and (&gt; m 2) (leap? y)) 1 0)))</p><p>(defun year-num (y)<br />&#160; (let ((d 0))<br />&#160; &#160; (if (&gt;= y yzero)<br />&#160; &#160; &#160; &#160; (dotimes (i (- y yzero) d)<br />&#160; &#160; &#160; &#160; &#160; (incf d (year-days (+ yzero i))))<br />&#160; &#160; &#160; &#160; (dotimes (i (- yzero y) (- d))<br />&#160; &#160; &#160; &#160; &#160; (incf d (year-days (+ y i)))))))</p><p>(defun year-days (y) (if (leap? y) 366 365))</p><p>图 5.1 日期运算：转换日期至数字</p><p>典型 Lisp 程序的生命周期有四个阶段：先写好，然后读入，接着编译，最后执行。有件 Lisp 非常独特的事情之一是，在这四个阶段时， Lisp 一直都在那里。可以在你的程序编译 (参见 10.2 小节)或读入时 (参见 14.3 小节) 来调用 Lisp。我们推导出 month 的过程演示了，如何在撰写一个程序时使用 Lisp。</p><p>效率通常只跟第四个阶段有关系，运行期（run-time）。在前三个阶段，你可以随意的使用列表拥有的威力与灵活性，而不需要担心效率。</p><p>若你使用图 5.1 的代码来造一个时光机器（time machine），当你抵达时，人们大概会不同意你的日期。即使是相对近的现在，欧洲的日期也曾有过偏移，因为人们会获得更精准的每年有多长的概念。在说英语的国家，最后一次的不连续性出现在 1752 年，日期从 9 月 2 日跳到 9 月 14 日。</p><p>每年有几天取决于该年是否是润年。如果该年可以被四整除，我们说该年是润年，除非该年可以被 100 整除，则该年非润年 ── 而要是它可以被 400 整除，则又是润年。所以 1904 年是润年，1900 年不是，而 1600 年是。</p><p>要决定某个数是否可以被另个数整除，我们使用函数 mod ，返回相除后的余数：</p><p>&gt; (mod 23 5)<br />3<br />&gt; (mod 25 5)<br />0</p><p>如果第一个实参除以第二个实参的余数为 0，则第一个实参是可以被第二个实参整除的。函数 leap? 使用了这个方法，来决定它的实参是否是一个润年：</p><p>&gt; (mapcar #&#039;leap? &#039;(1904 1900 1600))<br />(T NIL T)</p><p>我们用来转换日期至整数的函数是 date-&gt;num 。它返回日期中每个单位的天数总和。要找到从某月份开始的天数和，我们调用month-num ，它在 month 中查询天数，如果是在润年的二月之后，则加一。</p><p>要找到从某年开始的天数和， date-&gt;num 调用 year-num ，它返回某年一月一日相对于起始点（2000.01.01）所代表的天数。这个函数的工作方式是从传入的实参 y 年开始，朝着起始年（2000）往上或往下数。</p><p>(defun num-&gt;date (n)<br />&#160; (multiple-value-bind (y left) (num-year n)<br />&#160; &#160; (multiple-value-bind (m d) (num-month left y)<br />&#160; &#160; &#160; (values d m y))))</p><p>(defun num-year (n)<br />&#160; (if (&lt; n 0)<br />&#160; &#160; &#160; (do* ((y (- yzero 1) (- y 1))<br />&#160; &#160; &#160; &#160; &#160; &#160; (d (- (year-days y)) (- d (year-days y))))<br />&#160; &#160; &#160; &#160; &#160; &#160;((&lt;= d n) (values y (- n d))))<br />&#160; &#160; &#160; (do* ((y yzero (+ y 1))<br />&#160; &#160; &#160; &#160; &#160; &#160; (prev 0 d)<br />&#160; &#160; &#160; &#160; &#160; &#160; (d (year-days y) (+ d (year-days y))))<br />&#160; &#160; &#160; &#160; &#160; &#160;((&gt; d n) (values y (- n prev))))))</p><p>(defun num-month (n y)<br />&#160; (if (leap? y)<br />&#160; &#160; &#160; (cond ((= n 59) (values 2 29))<br />&#160; &#160; &#160; &#160; &#160; &#160; ((&gt; n 59) (nmon (- n 1)))<br />&#160; &#160; &#160; &#160; &#160; &#160; (t&#160; &#160; &#160; &#160; (nmon n)))<br />&#160; &#160; &#160; (nmon n)))</p><p>(defun nmon (n)<br />&#160; (let ((m (position n month :test #&#039;&lt;)))<br />&#160; &#160; (values m (+ 1 (- n (svref month (- m 1)))))))</p><p>(defun date+ (d m y n)<br />&#160; (num-&gt;date (+ (date-&gt;num d m y) n)))</p><p>图 5.2 日期运算：转换数字至日期</p><p>图 5.2 展示了代码的下半部份。函数 num-&gt;date 将整数转换回日期。它调用了 num-year 函数，以日期的格式返回年，以及剩余的天数。再将剩余的天数传给 num-month ，分解出月与日。</p><p>和 year-num 相同， num-year 从起始年往上或下数，一次数一年。并持续累积天数，直到它获得一个绝对值大于或等于 n 的数。如果它往下数，那么它可以返回当前迭代中的数值。不然它会超过年份，然后必须返回前次迭代的数值。这也是为什么要使用 prev ，prev 在每次迭代时会存入 days 前次迭代的数值。</p><p>函数 num-month 以及它的子程序（subroutine） nmon 的行为像是相反地 month-num 。他们从常数向量 month 的数值到位置，然而month-num 从位置到数值。</p><p>图 5.2 的前两个函数可以合而为一。与其返回数值给另一个函数， num-year 可以直接调用 num-month 。现在分成两部分的代码，比较容易做交互测试，但是现在它可以工作了，下一步或许是把它合而为一。</p><p>有了 date-&gt;num 与 num-&gt;date ，日期运算是很简单的。我们在 date+ 里使用它们，可以从特定的日期做加减。如果我们想透过date+ 来知道 1997 年 12 月 17 日六十天之后的日期:</p><p>&gt; (multiple-value-list (date+ 17 12 1997 60))<br />(15 2 1998)</p><p>我们得到，1998 年 2 月 15 日。<br />Chapter 5 总结 (Summary)</p><p>&#160; &#160; Common Lisp 有三个基本的区块建构子： progn ；允许返回的 block ；以及允许 goto 的 tagbody 。很多内置的操作符隐含在区块里。<br />&#160; &#160; 进入一个新的词法语境，概念上等同于函数调用。<br />&#160; &#160; Common Lisp 提供了适合不同情况的条件式。每个都可以使用 if 来定义。<br />&#160; &#160; 有数个相似迭代操作符的变种。<br />&#160; &#160; 表达式可以返回多个数值。<br />&#160; &#160; 计算过程可以被中断以及保护，保护可使其免于中断所造成的后果。</p><p>Chapter 5 练习 (Exercises)</p><p>&#160; &#160; 将下列表达式翻译成没有使用 let 与 let* ，并使同样的表达式不被求值 2 次。</p><p>(a) (let ((x (car y)))<br />&#160; &#160; &#160; (cons x x))<br />(b) (let* ((w (car x))<br />&#160; &#160; &#160; &#160; &#160; &#160;(y (+ w z)))<br />&#160; &#160; &#160; (cons w y))</p><p>&#160; &#160; 使用 cond 重写 29 页的 mystery 函数。（译注: 第二章的练习第 5 题的 (b) 部分)<br />&#160; &#160; 定义一个返回其实参平方的函数，而当实参是一个正整数且小于等于 5 时，不要计算其平方。<br />&#160; &#160; 使用 case 与 svref 重写 month-num (图 5.1)。<br />&#160; &#160; 定义一个迭代与递归版本的函数，接受一个对象 x 与向量 v ，并返回一个列表，包含了向量 v 当中，所有直接在 x 之前的对象：</p><p>&gt; (precedes #\a &quot;abracadabra&quot;)<br />(#\c #\d #\r)</p><p>&#160; &#160; 定义一个迭代与递归版本的函数，接受一个对象与列表，并返回一个新的列表，在原本列表的对象之间加上传入的对象：</p><p>&gt; (intersperse &#039;- &#039;(a b c d))<br />(A - B - C - D)</p><p>&#160; &#160; 定义一个接受一系列数字的函数，并在若且唯若每一对（pair）数字的差为一时，返回真，使用</p><p>(a) 递归<br />(b) do<br />(c) mapc 与 return</p><p>&#160; &#160; 定义一个单递归函数，返回两个值，分别是向量的最大与最小值。<br />&#160; &#160; 图 3.12 的程序在找到一个完整的路径时，仍持续遍历伫列。在搜索范围大时，这可能会产生问题。</p><p>(a) 使用 catch 与 throw 来变更程序，使其找到第一个完整路径时，直接返回它。<br />(b) 重写一个做到同样事情的程序，但不使用 catch 与 throw。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 18 Nov 2022 13:08:10 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?pid=611#p611</guid>
		</item>
	</channel>
</rss>
