<?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=580&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo-zh / ANSI Common Lisp 第十四章：进阶议题]]></title>
		<link>http://www.gentoo-zh.org/viewtopic.php?id=580</link>
		<description><![CDATA[ANSI Common Lisp 第十四章：进阶议题 最近发表的帖子。]]></description>
		<lastBuildDate>Fri, 18 Nov 2022 13:14:11 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[ANSI Common Lisp 第十四章：进阶议题]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?pid=620#p620</link>
			<description><![CDATA[<p>本章是选择性阅读的。本章描述了 Common Lisp 里一些更深奥的特性。Common Lisp 像是一个冰山：大部分的功能对于那些永远不需要他们的多数用户是看不见的。你或许永远不需要自己定义包 (Package)或读取宏 (read-macros)，但当你需要时，有些例子可以让你参考是很有用的。</p><p>&#160; &#160; &#160; &#160; 14.1 类型标识符 (Type Specifiers)<br />&#160; &#160; &#160; &#160; 14.2 二进制流 (Binary Streams)<br />&#160; &#160; &#160; &#160; 14.3 读取宏 (Read-Macros)<br />&#160; &#160; &#160; &#160; 14.4 包 (Packages)<br />&#160; &#160; &#160; &#160; 14.5 Loop 宏 (The Loop Facility)<br />&#160; &#160; &#160; &#160; 14.6 状况 (Conditions)</p><p>14.1 类型标识符 (Type Specifiers)</p><p>类型在 Common Lisp 里不是对象。举例来说，没有对象对应到 integer 这个类型。我们像是从 type-of 函数里所获得的，以及作为传给像是 typep 函数的参数，不是一个类型，而是一个类型标识符 (type specifier)。</p><p>一个类型标识符是一个类型的名称。最简单的类型标识符是像是 integer 的符号。这些符号形成了 Common Lisp 里的类型层级。在层级的最顶端是类型 t ── 所有的对象皆为类型 t 。而类型层级不是一棵树。从 nil 至顶端有两条路，举例来说：一条从 atom，另一条从 list 与 sequence 。</p><p>一个类型实际上只是一个对象集合。这意味著有多少类型就有多少个对象的集合：一个无穷大的数目。我们可以用原子的类型标识符 (atomic type specifiers)来表示某些集合：比如 integer 表示所有整数集合。但我们也可以建构一个复合类型标识符 (compound type specifiers)来参照到任何对象的集合。</p><p>举例来说，如果 a 与 b 是两个类型标识符，则 (or a b) 表示分别由 a 与 b 类型所表示的联集 (union)。也就是说，一个类型(or a b) 的对象是类型 a 或 类型 b 。</p><p>如果 circular? 是一个对于 cdr 为环状的列表返回真的函数，则你可以使用适当的序列集合来表示： [1]</p><p>(or vector (and list (not (satisfies circular?))))</p><p>某些原子的类型标识符也可以出现在复合类型标识符。要表示介于 1 至 100 的整数（包含），我们可以用：</p><p>(integer 1 100)</p><p>这样的类型标识符用来表示一个有限的类型 (finite type)。</p><p>在一个复合类型标识符里，你可以通过在一个参数的位置使用 * 来留下某些未指定的信息。所以</p><p>(simple-array fixnum (* *))</p><p>描述了指定给 fixnum 使用的二维简单数组 (simple array)集合，而</p><p>(simple-array fixnum *)</p><p>描述了指定给 finxnum 使用的简单数组集合 (前者的超类型 「supertype」)。尾随的星号可以省略，所以上个例子可以写为：</p><p>(simple-array fixnum)</p><p>若一个复合类型标识符没有传入参数，你可以使用一个原子。所以 simple-array 描述了所有简单数组的集合。</p><p>如果有某些复合类型标识符你想重复使用，你可以使用 deftype 定义一个缩写。这个宏与 defmacro 相似，但会展开成一个类型标识符，而不是一个表达式。通过表达</p><p>(deftype proseq ()<br />&#160; &#160; &#160; &#160; &#039;(or vector (and list (not (satisfies circular?)))))</p><p>我们定义了 proseq 作为一个新的原子类型标识符：</p><p>&gt; (typep #(1 2) &#039;proseq)<br />T</p><p>如果你定义一个接受参数的类型标识符，参数会被视为 Lisp 形式（即没有被求值），与 defmacro 一样。所以</p><p>(deftype multiple-of (n)<br />&#160; `(and integer (satisfies (lambda (x)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(zerop (mod x ,n))))))</p><p>(译注: 注意上面代码是使用反引号 ````` )</p><p>定义了 (multiple-of n) 当成所有 n 的倍数的标识符：</p><p>&gt; (type 12 &#039;(multiple-of 4))<br />T</p><p>类型标识符会被直译 (interpreted)，因此很慢，所以通常你最好定义一个函数来处理这类的测试。<br />14.2 二进制流 (Binary Streams)</p><p>第 7 章曾提及的流有二进制流 (binary streams)以及字符流 (character streams)。一个二进制流是一个整数的来源及/或终点，而不是字符。你通过指定一个整数的子类型来创建一个二进制流 ── 当你打开流时，通常是用 unsigned-byte ── 来作为 :element-type 的参数。</p><p>关于二进制流的 I/O 函数仅有两个， read-byte 以及 write-byte 。所以下面是如何定义复制一个文件的函数：</p><p>(defun copy-file (from to)<br />&#160; (with-open-file (in from :direction :input<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;:element-type &#039;unsigned-byte)<br />&#160; &#160; (with-open-file (out to :direction :output<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; :element-type &#039;unsigned-byte)<br />&#160; &#160; &#160; (do ((i (read-byte in nil -1)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (read-byte in nil -1)))<br />&#160; &#160; &#160; &#160; &#160; ((minusp i))<br />&#160; &#160; &#160; &#160; (declare (fixnum i))<br />&#160; &#160; &#160; &#160; (write-byte i out)))))</p><p>仅通过指定 unsigned-byte 给 :element-type ，你让操作系统选择一个字节 (byte)的长度。举例来说，如果你明确地想要读写 7 比特的整数，你可以使用：</p><p>(unsigned-byte 7)</p><p>来传给 :element-type 。<br />14.3 读取宏 (Read-Macros)</p><p>7.5 节介绍过宏字符 (macro character)的概念，一个对于 read 有特别意义的字符。每一个这样的字符，都有一个相关联的函数，这函数告诉 read 当遇到这个字符时该怎么处理。你可以变更某个已存在宏字符所相关联的函数，或是自己定义新的宏字符。</p><p>函数 set-macro-character 提供了一种方式来定义读取宏 (read-macros)。它接受一个字符及一个函数，因此当 read 碰到该字符时，它返回调用传入函数后的结果。</p><p>Lisp 中最古老的读取宏之一是 &#039; ，即 quote 。我们可以定义成：</p><p>(set-macro-character #\&#039;<br />&#160; &#160; &#160; &#160; #&#039;(lambda (stream char)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (list (quote quote) (read stream t nil t))))</p><p>当 read 在一个普通的语境下遇到 &#039; 时，它会返回在当前流和字符上调用这个函数的结果。(这个函数忽略了第二个参数，第二个参数永远是引用字符。)所以当 read 看到 &#039;a 时，会返回 (quote a) 。</p><p>译注: read 函数接受的参数 (read &amp;optional stream eof-error eof-value recursive)</p><p>现在我们明白了 read 最后一个参数的用途。它表示无论 read 调用是否在另一个 read 里。传给 read 的参数在几乎所有的读取宏里皆相同：传入参数有流 (stream)；接著是第二个参数， t ，说明了 read 若读入的东西是 end-of-file 时，应不应该报错；第三个参数说明了不报错时要返回什么，因此在这里也就不重要了；而第四个参数 t 说明了这个 read 调用是递归的。</p><p>(译注：困惑的话可以看看 read 的定义 )</p><p>你可以（通过使用 make-dispatch-macro-character ）来定义你自己的派发宏字符（dispatching macro character），但由于 #已经是一个宏字符，所以你也可以直接使用。六个 # 打头的组合特别保留给你使用： #! 、 #? 、 ##[ 、 ##] 、 #{ 、 #} 。</p><p>你可以通过调用 set-dispatch-macro-character 定义新的派发宏字符组合，与 set-macro-character 类似，除了它接受两个字符参数外。下面的代码定义了 #? 作为返回一个整数列表的读取宏。</p><p>(set-dispatch-macro-character #\# #\?<br />&#160; #&#039;(lambda (stream char1 char2)<br />&#160; &#160; &#160; (list &#039;quote<br />&#160; &#160; &#160; &#160; &#160; &#160; (let ((lst nil))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (dotimes (i (+ (read stream t nil t) 1))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (push i lst))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (nreverse lst)))))</p><p>现在 #?n 会被读取成一个含有整数 0 至 n 的列表。举例来说：</p><p>&gt; #?7<br />(1 2 3 4 5 6 7)</p><p>除了简单的宏字符，最常定义的宏字符是列表分隔符 (list delimiters)。另一个保留给用户的字符组是 #{ 。以下我们定义了一种更复杂的左括号：</p><p>(set-macro-character #\} (get-macro-character #\)))</p><p>(set-dispatch-macro-character #\# #\{<br />&#160; #&#039;(lambda (stream char1 char2)<br />&#160; &#160; &#160; (let ((accum nil)<br />&#160; &#160; &#160; &#160; &#160; &#160; (pair (read-delimited-list #\} stream t)))<br />&#160; &#160; &#160; &#160; (do ((i (car pair) (+ i 1)))<br />&#160; &#160; &#160; &#160; &#160; &#160; ((&gt; i (cadr pair))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160;(list &#039;quote (nreverse accum)))<br />&#160; &#160; &#160; &#160; &#160; (push i accum)))))</p><p>这定义了一个这样形式 #{x y} 的表达式，使得这样的表达式被读取为所有介于 x 与 y 之间的整数列表，包含 x 与 y ：</p><p>&gt; #{2 7}<br />(2 3 4 4 5 6 7)</p><p>函数 read-delimited-list 正是为了这样的读取宏而生的。它的第一个参数是被视为列表结束的字符。为了使 } 被识别为分隔符，必须先给它这个角色，所以程序在开始的地方调用了 set-macro-character 。</p><p>如果你想要在定义一个读取宏的文件里使用该读取宏，则读取宏的定义应要包在一个 eval-when 表达式里，来确保它在编译期会被求值。不然它的定义会被编译，但不会被求值，直到编译文件被载入时才会被求值。<br />14.4 包 (Packages)</p><p>一个包是一个将名字映对到符号的 Lisp 对象。当前的包总是存在全局变量 *package* 里。当 Common Lisp 启动时，当前的包会是*common-lisp-user* ，通常称为用户包 (user package)。函数 package-name 返回包的名字，而 find-package 返回一个给定名称的包:</p><p>&gt; (package-name *package*)<br />&quot;COMMON-LISP-USER&quot;<br />&gt; (find-package &quot;COMMON-LISP-USER&quot;)<br />#&lt;Package &quot;COMMON-LISP-USER&quot; 4CD15E&gt;</p><p>通常一个符号在读入时就被 interned 至当前的包里面了。函数 symbol-package 接受一个符号并返回该符号被 interned 的包。</p><p>(symbol-package &#039;sym)<br />#&lt;Package &quot;COMMON-LISP-USER&quot; 4CD15E&gt;</p><p>有趣的是，这个表达式返回它该返回的值，因为表达式在可以被求值前必须先被读入，而读取这个表达式导致 sym 被 interned。为了之后的用途，让我们给 sym 一个值:</p><p>&gt; (setf sym 99)<br />99</p><p>现在我们可以创建及切换至一个新的包：</p><p>&gt; (setf *package* (make-package &#039;mine<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; :use &#039;(common-lisp)))<br />#&lt;Package &quot;MINE&quot; 63390E&gt;</p><p>现在应该会听到诡异的背景音乐，因为我们来到一个不一样的世界了： 在这里 sym 不再是本来的 sym 了。</p><p>MINE&gt; sym<br />Error: SYM has no value</p><p>为什么会这样？因为上面我们设为 99 的 sym 与 mine 里的 sym 是两个不同的符号。 [2] 要在用户包之外参照到原来的 sym ，我们必须把包的名字加上两个冒号作为前缀：</p><p>MINE&gt; common-lisp-user::sym<br />99</p><p>所以有着相同打印名称的不同符号能够在不同的包内共存。可以有一个 sym 在 common-lisp-user 包，而另一个 sym 在 mine 包，而他们会是不一样的符号。这就是包存在的意义。如果你在分开的包内写你的程序，你大可放心选择函数与变量的名字，而不用担心某人使用了同样的名字。即便是他们使用了同样的名字，也不会是相同的符号。</p><p>包也提供了信息隐藏的手段。程序应通过函数与变量的名字来参照它们。如果你不让一个名字在你的包之外可见的话，那么另一个包中的代码就无法使用或者修改这个名字所参照的对象。</p><p>通常使用两个冒号作为包的前缀也是很差的风格。这么做你就违反了包本应提供的模块性。如果你不得不使用一个双冒号来参照到一个符号，这是因为某人根本不想让你用。</p><p>通常我们应该只参照被输出 ( exported )的符号。如果我们回到用户包里，并输出一个被 interned 的符号，</p><p>MINE&gt; (in-package common-lisp-user)<br />#&lt;Package &quot;COMMON-LISP-USER&quot; 4CD15E&gt;<br />&gt; (export &#039;bar)<br />T<br />&gt; (setf bar 5)<br />5</p><p>我们使这个符号对于其它的包是可视的。现在当我们回到 mine ，我们可以仅使用单冒号来参照到 bar ，因为他是一个公开可用的名字：</p><p>&gt; (in-package mine)<br />#&lt;Package &quot;MINE&quot; 63390E&gt;<br />MINE&gt; common-lisp-user:bar<br />5</p><p>通过把 bar 输入 ( import )至 mine 包，我们就能进一步让 mine 和 user 包可以共享 bar 这个符号：</p><p>MINE&gt; (import &#039;common-lisp-user:bar)<br />T<br />MINE&gt; bar<br />5</p><p>在输入 bar 之后，我们根本不需要用任何包的限定符 (package qualifier)，就能参照它了。这两个包现在共享了同样的符号；不可能会有一个独立的 mine:bar 了。</p><p>要是已经有一个了怎么办？在这种情况下， import 调用会产生一个错误，如下面我们试著输入 sym 时便知：</p><p>MINE&gt; (import &#039;common-lisp-user::sym)<br />Error: SYM is already present in MINE.</p><p>在此之前，当我们试着在 mine 包里对 sym 进行了一次不成功的求值，我们使 sym 被 interned 至 mine 包里。而因为它没有值，所以产生了一个错误，但输入符号名的后果就是使这个符号被 intern 进这个包。所以现在当我们试著输入 sym 至 mine 包里，已经有一个相同名称的符号了。</p><p>另一个方法来获得别的包内符号的存取权是使用( use )它：</p><p>MINE&gt; (use-package &#039;common-lisp-user)<br />T</p><p>现在所有由用户包 (译注: common-lisp-user 包）所输出的符号，可以不需要使用任何限定符在 mine 包里使用。(如果 sym 已经被用户包输出了，这个调用也会产生一个错误。)</p><p>含有自带操作符及变量名字的包叫做 common-lisp 。由于我们将这个包的名字在创建 mine 包时作为 make-package 的 :use 参数，所有的 Common Lisp 自带的名字在 mine 里都是可视的:</p><p>MINE&gt; #&#039;cons<br />#&lt;Compiled-Function CONS 462A3E&gt;</p><p>在编译后的代码中, 通常不会像这样在顶层进行包的操作。更常见的是包的调用会包含在源文件里。通常，只要把 in-package 和defpackage 放在源文件的开头就可以了，正如 137 页所示。</p><p>这种由包所提供的模块性实际上有点奇怪。我们不是对象的模块 (modules)，而是名字的模块。</p><p>每一个使用了 common-lisp 的包，都可以存取 cons ，因为 common-lisp 包里有一个叫这个名字的函数。但这会导致一个名字为cons 的变量也会在每个使用了 common-lisp 包里是可视的。如果包使你困惑，这就是主要的原因；因为包不是基于对象而是基于名字。<br />14.5 Loop 宏 (The Loop Facility)</p><p>loop 宏最初是设计来帮助无经验的 Lisp 用户来写出迭代的代码。与其撰写 Lisp 代码，你用一种更接近英语的形式来表达你的程序，然后这个形式被翻译成 Lisp。不幸的是， loop 比原先设计者预期的更接近英语：你可以在简单的情况下使用它，而不需了解它是如何工作的，但想在抽象层面上理解它几乎是不可能的。</p><p>如果你是曾经计划某天要理解 loop 怎么工作的许多 Lisp 程序员之一，有一些好消息与坏消息。好消息是你并不孤单：几乎没有人理解它。坏消息是你永远不会理解它，因为 ANSI 标准实际上并没有给出它行为的正式规范。</p><p>这个宏唯一的实际定义是它的实现方式，而唯一可以理解它（如果有人可以理解的话）的方法是通过实例。ANSI 标准讨论 loop 的章节大部分由例子组成，而我们将会使用同样的方式来介绍相关的基础概念。</p><p>第一个关于 loop 宏我们要注意到的是语法 ( syntax )。一个 loop 表达式不是包含子表达式而是子句 (clauses)。這些子句不是由括号分隔出来；而是每种都有一个不同的语法。在这个方面上， loop 与传统的 Algol-like 语言相似。但其它 loop 独特的特性，使得它与 Algol 不同，也就是在 loop 宏里调换子句的顺序与会发生的事情没有太大的关联。</p><p>一个 loop 表达式的求值分为三个阶段，而一个给定的子句可以替多于一个的阶段贡献代码。这些阶段如下：</p><p>&#160; &#160; 序幕 (Prologue)。 被求值一次来做为迭代过程的序幕。包括了将变量设至它们的初始值。<br />&#160; &#160; 主体 (Body) 每一次迭代时都会被求值。<br />&#160; &#160; 闭幕 (Epilogue) 当迭代结束时被求值。决定了 loop 表达式的返回值（可能返回多个值）。</p><p>我们会看几个 loop 子句的例子，并考虑何种代码会贡献至何个阶段。</p><p>举例来说，最简单的 loop 表达式，我们可能会看到像是下列的代码：</p><p>&gt; (loop for x from 0 to 9<br />&#160; &#160; &#160; &#160; do (princ x))<br />0123456789<br />NIL</p><p>这个 loop 表达式印出从 0 至 9 的整数，并返回 nil 。第一个子句，</p><p>for x from 0 to 9</p><p>贡献代码至前两个阶段，导致 x 在序幕中被设为 0 ，在主体开头与 9 来做比较，在主体结尾被递增。第二个子句，</p><p>do (princ x)</p><p>贡献代码给主体。</p><p>一个更通用的 for 子句说明了起始与更新的形式 (initial and update form)。停止迭代可以被像是 while 或 until 子句来控制。</p><p>&gt; (loop for x = 8 then (/ x 2)<br />&#160; &#160; &#160; &#160; until (&lt; x 1)<br />&#160; &#160; &#160; &#160; do (princ x))<br />8421<br />NIL</p><p>你可以使用 and 来创建复合的 for 子句，同时初始及更新两个变量：</p><p>&gt; (loop for x from 1 to 4<br />&#160; &#160; &#160; &#160; and y from 1 to 4<br />&#160; &#160; &#160; &#160; do (princ (list x y)))<br />(1 1)(2 2)(3 3)(4 4)<br />NIL</p><p>要不然有多重 for 子句时，变量会被循序更新。</p><p>另一件在迭代代码通常会做的事是累积某种值。举例来说：</p><p>&gt; (loop for x in &#039;(1 2 3 4)<br />&#160; &#160; &#160; &#160; collect (1+ x))<br />(2 3 4 5)</p><p>在 for 子句使用 in 而不是 from ，导致变量被设为一个列表的后续元素，而不是连续的整数。</p><p>在这个情况里， collect 子句贡献代码至三个阶段。在序幕，一個匿名累加器 (anonymous accumulator)設為 nil ；在主体裡，(1+ x) 被累加至這個累加器，而在闭幕时返回累加器的值。</p><p>这是返回一个特定值的第一个例子。有用来明确指定返回值的子句，但没有这些子句时，一个 collect 子句决定了返回值。所以我们在这里所做的其实是重复了 mapcar 。</p><p>loop 最常见的用途大概是蒐集调用一个函数数次的结果：</p><p>&gt; (loop for x from 1 to 5<br />&#160; &#160; &#160; &#160; collect (random 10))<br />(3 8 6 5 0)</p><p>这里我们获得了一个含五个随机数的列表。这跟我们定义过的 map-int 情况类似 (105 页「译注: 6.4 小节。」)。如果我们有了 loop，为什么还需要 map-int ？另一个人也可以说，如果我们有了 map-int ，为什么还需要 loop ？</p><p>一个 collect 子句也可以累积值到一个有名字的变量上。下面的函数接受一个数字的列表并返回偶数与奇数列表：</p><p>(defun even/odd (ns)<br />&#160; (loop for n in ns<br />&#160; &#160; &#160; &#160; if (evenp n)<br />&#160; &#160; &#160; &#160; &#160; &#160;collect n into evens<br />&#160; &#160; &#160; &#160; &#160; &#160;else collect n into odds<br />&#160; &#160; &#160; &#160; finally (return (values evens odds))))</p><p>一个 finally 子句贡献代码至闭幕。在这个情况它指定了返回值。</p><p>一个 sum 子句和一个 collect 子句类似，但 sum 子句累积一个数字，而不是一个列表。要获得 1 至 n 的和，我们可以写：</p><p>(defun sum (n)<br />&#160; (loop for x from 1 to n<br />&#160; &#160; &#160; &#160; sum x))</p><p>loop 更进一步的细节在附录 D 讨论，从 325 页开始。举个例子，图 14.1 包含了先前章节的两个迭代函数，而图 14.2 演示了将同样的函数翻译成 loop 。</p><p>(defun most (fn lst)<br />&#160; (if (null lst)<br />&#160; &#160; &#160; (values nil nil)<br />&#160; &#160; &#160; (let* ((wins (car lst))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160;(max (funcall fn wins)))<br />&#160; &#160; &#160; &#160; (dolist (obj (cdr lst))<br />&#160; &#160; &#160; &#160; &#160; (let ((score (funcall fn obj)))<br />&#160; &#160; &#160; &#160; &#160; &#160; (when (&gt; score max)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (setf wins obj<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; max&#160; score))))<br />&#160; &#160; &#160; &#160; (values wins max))))</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>图 14.1 不使用 loop 的迭代函数</p><p>(defun most (fn lst)<br />&#160; (if (null lst)<br />&#160; &#160; &#160; (values nil nil)<br />&#160; &#160; &#160; (loop with wins = (car lst)<br />&#160; &#160; &#160; &#160; &#160; &#160; with max = (funcall fn wins)<br />&#160; &#160; &#160; &#160; &#160; &#160; for obj in (cdr lst)<br />&#160; &#160; &#160; &#160; &#160; &#160; for score = (funcall fn obj)<br />&#160; &#160; &#160; &#160; &#160; &#160; when (&gt; score max)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(do (setf wins obj<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;max score)<br />&#160; &#160; &#160; &#160; &#160; &#160; finally (return (values wins max))))))</p><p>(defun num-year (n)<br />&#160; (if (&lt; n 0)<br />&#160; &#160; &#160; (loop for y downfrom (- yzero 1)<br />&#160; &#160; &#160; &#160; &#160; &#160; until (&lt;= d n)<br />&#160; &#160; &#160; &#160; &#160; &#160; sum (- (year-days y)) into d<br />&#160; &#160; &#160; &#160; &#160; &#160; finally (return (values (+ y 1) (- n d))))<br />&#160; &#160; &#160; (loop with prev = 0<br />&#160; &#160; &#160; &#160; &#160; &#160; for y from yzero<br />&#160; &#160; &#160; &#160; &#160; &#160; until (&gt; d n)<br />&#160; &#160; &#160; &#160; &#160; &#160; do (setf prev d)<br />&#160; &#160; &#160; &#160; &#160; &#160; sum (year-days y) into d<br />&#160; &#160; &#160; &#160; &#160; &#160; finally (return (values (- y 1)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (- n prev))))))</p><p>图 14.2 使用 loop 的迭代函数</p><p>一个 loop 的子句可以参照到由另一个子句所设置的变量。举例来说，在 even/odd 的定义里面， finally 子句参照到由两个collect 子句所创建的变量。这些变量之间的关系，是 loop 定义最含糊不清的地方。考虑下列两个表达式：</p><p>(loop for y = 0 then z<br />&#160; &#160; &#160; for x from 1 to 5<br />&#160; &#160; &#160; sum 1 into z<br />&#160; &#160; &#160; finally (return y z))</p><p>(loop for x from 1 to 5<br />&#160; &#160; &#160; for y = 0 then z<br />&#160; &#160; &#160; sum 1 into z<br />&#160; &#160; &#160; finally (return y z))</p><p>它们看起来够简单 ── 每一个有四个子句。但它们返回同样的值吗？它们返回的值多少？你若试着在标准中想找答案将徒劳无功。每一个 loop 子句本身是够简单的。但它们组合起来的方式是极为复杂的 ── 而最终，甚至标准里也没有明确定义。</p><p>由于这类原因，使用 loop 是不推荐的。推荐 loop 的理由，你最多可以说，在像是图 14.2 这般经典的例子中， loop 让代码看起来更容易理解。<br />14.6 状况 (Conditions)</p><p>在 Common Lisp 里，状况 (condition)包括了错误以及其它可能在执行期发生的情况。当一个状况被捕捉时 (signalled)，相应的处理程序 (handler)会被调用。处理错误状况的缺省处理程序通常会调用一个中断循环 (break-loop)。但 Common Lisp 提供了多样的操作符来捕捉及处理错误。要覆写缺省的处理程序，甚至是自己写一个新的处理程序也是有可能的。</p><p>多数的程序员不会直接处理状况。然而有许多更抽象的操作符使用了状况，而要了解这些操作符，知道背后的原理是很有用的。</p><p>Common lisp 有数个操作符用来捕捉错误。最基本的是 error 。一个调用它的方法是给入你会给 format 的相同参数：</p><p>&gt; (error &quot;Your report uses ~A as a verb.&quot; &#039;status)<br />Error: Your report uses STATUS as a verb<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;Options: :abort, :backtrace<br />&gt;&gt;</p><p>如上所示，除非这样的状况被处理好了，不然执行就会被打断。</p><p>用来捕捉错误的更抽象操作符包括了 ecase 、 check-type 以及 assert 。前者与 case 相似，要是没有键值匹配时会捕捉一个错误：</p><p>&gt; (ecase 1 (2 3) (4 5))<br />Error: No applicable clause<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;Options: :abort, :backtrace<br />&gt;&gt;</p><p>普通的 case 在没有键值匹配时会返回 nil ，但由于利用这个返回值是很差的编码风格，你或许会在当你没有 otherwise 子句时使用 ecase 。</p><p>check-type 宏接受一个位置，一个类型名以及一个选择性字符串，并在该位置的值不是预期的类型时，捕捉一个可修正的错误 (correctable error)。一个可修正错误的处理程序会给我们一个机会来提供一个新的值：</p><p>&gt; (let ((x &#039;(a b c)))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (check-type (car x) integer &quot;an integer&quot;)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; x)<br />Error: The value of (CAR X), A, should be an integer.<br />Options: :abort, :backtrace, :continue<br />&gt;&gt; :continue<br />New value of (CAR X)? 99<br />(99 B C)<br />&gt;</p><p>在这个例子里， (car x) 被设为我们提供的新值，并重新执行，返回了要是 (car x) 本来就包含我们所提供的值所会返回的结果。</p><p>这个宏是用更通用的 assert 所定义的， assert 接受一个测试表达式以及一个有着一个或多个位置的列表，伴随着你可能传给error 的参数：</p><p>&gt; (let ((sandwich &#039;(ham on rye)))<br />&#160; &#160; (assert (eql (car sandwich) &#039;chicken)<br />&#160; &#160; &#160; &#160; &#160; &#160; ((car sandwich))<br />&#160; &#160; &#160; &#160; &#160; &#160; &quot;I wanted a ~A sandwich.&quot; &#039;chicken)<br />&#160; &#160; sandwich)<br />Error: I wanted a CHICKEN sandwich.<br />Options: :abort, :backtrace, :continue<br />&gt;&gt; :continue<br />New value of (CAR SANDWICH)? &#039;chicken<br />(CHICKEN ON RYE)</p><p>要建立新的处理程序也是可能的，但大多数程序员只会间接的利用这个可能性，通过使用像是 ignore-errors 的宏。如果它的参数没产生错误时像在 progn 里求值一样，但要是在求值过程中，不管什么参数报错，执行是不会被打断的。取而代之的是， ignore-errors 表达式会直接返回两个值： nil 以及捕捉到的状况。</p><p>举例来说，如果在某个时候，你想要用户能够输入一个表达式，但你不想要在输入是语法上不合时中断执行，你可以这样写：</p><p>(defun user-input (prompt)<br />&#160; (format t prompt)<br />&#160; (let ((str (read-line)))<br />&#160; &#160; (or (ignore-errors (read-from-string str))<br />&#160; &#160; &#160; &#160; nil)))</p><p>若输入包含语法错误时，这个函数仅返回 nil :</p><p>&gt; (user-input &quot;Please type an expression&quot;)<br />Please type an expression&gt; #%@#+!!<br />NIL</p><p>脚注</p><p>[1] | 虽然标准没有提到这件事，你可以假定 and 以及 or 类型标示符仅考虑它们所要考虑的参数，与 or 及 and 宏类似。</p><p>[2] | 某些 Common Lisp 实现，当我们不在用户包下时，会在顶层提示符前打印包的名字。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 18 Nov 2022 13:14:11 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?pid=620#p620</guid>
		</item>
	</channel>
</rss>
