<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="https://www.gentoo-zh.org/extern.php?action=feed&amp;tid=581&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / ANSI Common Lisp 第十五章：示例：推论]]></title>
		<link>https://www.gentoo-zh.org/viewtopic.php?id=581</link>
		<description><![CDATA[ANSI Common Lisp 第十五章：示例：推论 最近发表的帖子。]]></description>
		<lastBuildDate>Fri, 18 Nov 2022 13:14:49 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[ANSI Common Lisp 第十五章：示例：推论]]></title>
			<link>https://www.gentoo-zh.org/viewtopic.php?pid=621#p621</link>
			<description><![CDATA[<p>接下来三章提供了大量的 Lisp 程序例子。选择这些例子来说明那些较长的程序所采取的形式，和 Lisp 所擅长解决的问题类型。</p><p>在这一章中我们将要写一个基于一组 if-then 规则的推论程序。这是一个经典的例子 —— 不仅在于其经常出现在教科书上，还因为它反映了 Lisp 作为一个“符号计算”语言的本意。这个例子散发着很多早期 Lisp 程序的气息。</p><p>&#160; &#160; &#160; &#160; 15.1 目标 (The Aim)<br />&#160; &#160; &#160; &#160; 15.2 匹配 (Matching)<br />&#160; &#160; &#160; &#160; 15.3 回答查询 (Answering Queries)<br />&#160; &#160; &#160; &#160; 15.4 分析 (Analysis)</p><p>15.1 目标 (The Aim)</p><p>在这个程序中，我们将用一种熟悉的形式来表示信息：包含单个判断式，以及跟在之后的零个或多个参数所组成的列表。要表示 Donald 是 Nancy 的家长，我们可以这样写：</p><p>(parent donald nancy)</p><p>事实上，我们的程序是要表示一些从已有的事实作出推断的规则。我们可以这样来表示规则：</p><p>(&lt;- head body)</p><p>其中， head 是 那么...部分 (then-part)， body 是 如果...部分 (if-part)。在 head 和 body 中我们使用以问号为前缀的符号来表示变量。所以下面这个规则：</p><p>(&lt;- (child ?x ?y) (parent ?y ?x))</p><p>表示：如果 y 是 x 的家长，那么 x 是 y 的孩子；更恰当地说，我们可以通过证明 (parent y x) 来证明 (child x y) 的所表示的事实。</p><p>可以把规则中的 body 部分(if-part) 写成一个复杂的表达式，其中包含 and , or 和 not 等逻辑操作。所以当我们想要表达 “如果 x 是 y 的家长，并且 x 是男性，那么 x 是 y 的父亲” 这样的规则，我们可以写：</p><p>(&lt;- (father ?x ?y) (and (parent ?x ?y) (male ?x)))</p><p>一些规则可能依赖另一些规则所产生的事实。比如，我们写的第一个规则是为了证明 (child x y) 的事实。如果我们定义如下规则：</p><p>(&lt;- (daughter ?x ?y) (and (child ?x ?y) (female ?x)))</p><p>然后使用它来证明 (daughter x y) 可能导致程序使用第一个规则去证明 (child x y) 。</p><p>表达式的证明可以回溯任意数量的规则，只要它最终结束于给出的已知事实。这个过程有时候被称为反向链接 (backward-chaining)。之所以说 反向 (backward) 是因为这一类推论先考虑 head 部分，这是为了在继续证明 body 部分之前检查规则是否有效。链接 (chaining) 来源于规则之间的依赖关系，从我们想要证明的内容到我们的已知条件组成一个链接 (尽管事实上它更像一棵树)。 λ<br />15.2 匹配 (Matching)</p><p>我们需要有一个函数来做模式匹配以完成我们的反向链接 (back-chaining) 程序，这个函数能够比较两个包含变量的列表，它会检查在给变量赋值后是否可以使两个列表相等。举例，如果 ?x 和 ?y 是变量，那么下面两个列表：</p><p>(p ?x ?y c ?x)<br />(p&#160; a&#160; b c&#160; a)</p><p>当 ?x = a 且 ?y = b 时匹配，而下面两个列表：</p><p>(p ?x b ?y a)<br />(p ?y b&#160; c a)</p><p>当 ?x = ?y = c 时匹配。</p><p>我们有一个 match 函数，它接受两棵树，如果这两棵树能匹配，则返回一个关联列表（assoc-list）来显示他们是如何匹配的：</p><p>(defun match (x y &amp;optional binds)<br />&#160; (cond<br />&#160; &#160;((eql x y) (values binds t))<br />&#160; &#160;((assoc x binds) (match (binding x binds) y binds))<br />&#160; &#160;((assoc y binds) (match x (binding y binds) binds))<br />&#160; &#160;((var? x) (values (cons (cons x y) binds) t))<br />&#160; &#160;((var? y) (values (cons (cons y x) binds) t))<br />&#160; &#160;(t<br />&#160; &#160; (when (and (consp x) (consp y))<br />&#160; &#160; &#160; (multiple-value-bind (b2 yes)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(match (car x) (car y) binds)<br />&#160; &#160; &#160; &#160; (and yes (match (cdr x) (cdr y) b2)))))))</p><p>(defun var? (x)<br />&#160; (and (symbolp x)<br />&#160; &#160; &#160; &#160;(eql (char (symbol-name x) 0) #\?)))</p><p>(defun binding (x binds)<br />&#160; (let ((b (assoc x binds)))<br />&#160; &#160; (if b<br />&#160; &#160; &#160; &#160; (or (binding (cdr b) binds)<br />&#160; &#160; &#160; &#160; &#160; &#160; (cdr b)))))</p><p>图 15.1: 匹配函数。</p><p>&gt; (match &#039;(p a b c a) &#039;(p ?x ?y c ?x))<br />((?Y . B) (?X . A))<br />T<br />&gt; (match &#039;(p ?x b ?y a) &#039;(p ?y b c a))<br />((?Y . C) (?X . ?Y))<br />T<br />&gt; (match &#039;(a b c) &#039;(a a a))<br />NIL</p><p>当 match 函数逐个元素地比较它的参数时候，它把 binds 参数中的值分配给变量，这被称为绑定 (bindings)。如果成功匹配，match 函数返回生成的绑定；否则，返回 nil 。当然并不是所有成功的匹配都会产生绑定，我们的 match 函数就像 gethash 函数那样返回第二个值来表明匹配成功：</p><p>&gt; (match &#039;(p ?x) &#039;(p ?x))<br />NIL<br />T</p><p>如果 match 函数像上面那样返回 nil 和 t ，表明这是一个没有产生绑定的成功匹配。下面用中文来描述 match 算法是如何工作的：</p><p>&#160; &#160; 如果 x 和 y 在 eql 上相等那么它们匹配；否则，<br />&#160; &#160; 如果 x 是一个已绑定的变量，并且绑定匹配 y ，那么它们匹配；否则，<br />&#160; &#160; 如果 y 是一个已绑定的变量，并且绑定匹配 x ，那么它们匹配；否则，<br />&#160; &#160; 如果 x 是一个未绑定的变量，那么它们匹配，并且为 x 建立一个绑定；否则，<br />&#160; &#160; 如果 y 是一个未绑定的变量，那么它们匹配，并且为 y 建立一个绑定；否则，<br />&#160; &#160; 如果 x 和 y 都是 cons ，并且它们的 car 匹配，由此产生的绑定又让 cdr 匹配，那么它们匹配。</p><p>下面是一个例子，按顺序来说明以上六种情况：</p><p>&gt; (match &#039;(p ?v&#160; b ?x&#160; d (?z ?z))<br />&#160; &#160; &#160; &#160; &#160;&#039;(p&#160; a ?w&#160; c ?y ( e&#160; e))<br />&#160; &#160; &#160; &#160; &#160;&#039;((?v . a) (?w . b)))<br />((?Z . E) (?Y . D) (?X . C) (?V . A) (?W . B))<br />T</p><p>match 函数通过调用 binding 函数在一个绑定列表中寻找变量（如果有的话）所关联的值。这个函数必须是递归的，因为有这样的情况 “匹配建立一个绑定列表，而列表中变量只是间接关联到它的值： ?x 可能被绑定到一个包含 (?x . ?y) 和 (?y . a) 的列表”：</p><p>&gt; (match &#039;(?x a) &#039;(?y ?y))<br />((?Y . A) (?X . ?Y))<br />T</p><p>先匹配 ?x 和 ?y ，然后匹配 ?y 和 a ，我们间接确定 ?x 是 a 。<br />15.3 回答查询 (Answering Queries)</p><p>在介绍了绑定的概念之后，我们可以更准确的说一下我们的程序将要做什么：它得到一个可能包含变量的表达式，根据我们给定的事实和规则返回使它正确的所有绑定。比如，我们只有下面这个事实：</p><p>(parent donald nancy)</p><p>然后我们想让程序证明：</p><p>(parent ?x ?y)</p><p>它会返回像下面这样的表达：</p><p>(((?x . donald) (?y . nancy)))</p><p>它告诉我们只有一个可以让这个表达式为真的方法： ?x 是 donald 并且 ?y 是 nancy 。</p><p>在通往目标的路上，我们已经有了一个的重要部分：一个匹配函数。 下面是用来定义规则的一段代码：</p><p>(defvar *rules* (make-hash-table))</p><p>(defmacro &lt;- (con &amp;optional ant)<br />&#160; `(length (push (cons (cdr &#039;,con) &#039;,ant)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(gethash (car &#039;,con) *rules*))))</p><p>图 15.2 定义规则</p><p>规则将被包含于一个叫做 *rules* 的哈希表，通过头部 (head) 的判断式构建这个哈系表。这样做加强了我们无法使用判断式中的变量的限制。虽然我们可以通过把所有这样的规则放在分离的列表中来消除限制，但是如果这样做，当我们需要证明某件事的时侯不得不和每一个列表进行匹配。</p><p>我们将要使用同一个宏 &lt;- 去定义事实 (facts)和规则 (rules)。一个事实将被表示成一个没有 body 部分的规则。这和我们对规则的定义保持一致。一个规则告诉我们你可以通过证明 body 部分来证明 head 部分，所以没有 body 部分的规则意味着你不需要通过证明任何东西来证明 head 部分。这里有两个对应的例子：</p><p>&gt; (&lt;- (parent donald nancy))<br />1<br />&gt; (&lt;- (child ?x ?y) (parent ?y ?x))<br />1</p><p>调用 &lt;- 返回的是给定判断式下存储的规则数量；用 length 函数来包装 push 能使我们免于看到顶层中的一大堆返回值。</p><p>下面是我们的推论程序所需的大多数代码：</p><p>(defun prove (expr &amp;optional binds)<br />&#160; (case (car expr)<br />&#160; &#160; (and (prove-and (reverse (cdr expr)) binds))<br />&#160; &#160; (or&#160; (prove-or (cdr expr) binds))<br />&#160; &#160; (not (prove-not (cadr expr) binds))<br />&#160; &#160; (t&#160; &#160;(prove-simple (car expr) (cdr expr) binds))))</p><p>(defun prove-simple (pred args binds)<br />&#160; (mapcan #&#039;(lambda (r)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (multiple-value-bind (b2 yes)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(match args (car r)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; binds)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (when yes<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (if (cdr r)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (prove (cdr r) b2)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (list b2)))))<br />&#160; &#160; &#160; &#160; &#160; (mapcar #&#039;change-vars<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (gethash pred *rules*))))</p><p>(defun change-vars (r)<br />&#160; (sublis (mapcar #&#039;(lambda (v) (cons v (gensym &quot;?&quot;)))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (vars-in r))<br />&#160; &#160; &#160; &#160; &#160; r))</p><p>(defun vars-in (expr)<br />&#160; (if (atom expr)<br />&#160; &#160; &#160; (if (var? expr) (list expr))<br />&#160; &#160; (union (vars-in (car expr))<br />&#160; &#160; &#160; &#160; &#160; &#160;(vars-in (cdr expr)))))</p><p>图 15.3: 推论。</p><p>上面代码中的 prove 函数是推论进行的枢纽。它接受一个表达式和一个可选的绑定列表作为参数。如果表达式不包含逻辑操作，它调用 prove-simple 函数，前面所说的链接 (chaining)正是在这个函数里产生的。这个函数查看所有拥有正确判断式的规则，并尝试对每一个规则的 head 部分和它想要证明的事实做匹配。对于每一个匹配的 head ，使用匹配所产生的新的绑定在 body 上调用 prove。对 prove 的调用所产生的绑定列表被 mapcan 收集并返回：</p><p>&gt; (prove-simple &#039;parent &#039;(donald nancy) nil)<br />(NIL)<br />&gt; (prove-simple &#039;child &#039;(?x ?y) nil)<br />(((#:?6 . NANCY) (#:?5 . DONALD) (?Y . #:?5) (?X . #:?6)))</p><p>以上两个返回值指出有一种方法可以证明我们的问题。（一个失败的证明将返回 nil。）第一个例子产生了一组空的绑定，第二个例子产生了这样的绑定： ?x 和 ?y 被（间接）绑定到 nancy 和 donald 。</p><p>顺便说一句，这是一个很好的例子来实践 2.13 节提出的观点。因为我们用函数式的风格来写这个程序，所以可以交互式地测试每一个函数。</p><p>第二个例子返回的值里那些 gensyms 是怎么回事？如果我们打算使用含有变量的规则，我们需要避免两个规则恰好包含相同的变量。如果我们定义如下两条规则：</p><p>(&lt;- (child ?x ?y) (parent ?y ?x))</p><p>(&lt;- (daughter ?y ?x) (and (child ?y ?x) (female ?y)))</p><p>第一条规则要表达的意思是：对于任何的 x 和 y ， 如果 y 是 x 的家长，则 x 是 y 的孩子。第二条则是：对于任何的 x 和 y ， 如果 y 是 x 的孩子并且 y 是女性，则 y 是 x 的女儿。在每一条规则内部，变量之间的关系是显著的，但是两条规则使用了相同的变量并非我们刻意为之。</p><p>如果我们使用上面所写的规则，它们将不会按预期的方式工作。如果我们尝试证明“ a 是 b 的女儿”，匹配到第二条规则的 head 部分时会将 a 绑定到 ?y ，将 b 绑定到 ?x。我们无法用这样的绑定匹配第一条规则的 head 部分：</p><p>&gt; (match &#039;(child ?y ?x)<br />&#160; &#160; &#160; &#160; &#160;&#039;(child ?x ?y)<br />&#160; &#160; &#160; &#160; &#160;&#039;((?y . a) (?x . b)))<br />NIL</p><p>为了保证一条规则中的变量只表示规则中各参数之间的关系，我们用 gensyms 来代替规则中的所有变量。这就是 change-vars 函数的目的。一个 gensym 不可能在另一个规则中作为变量出现。但是因为规则可以是递归的，我们必须防止出现一个规则和自身冲突的可能性，所以在定义和使用一个规则时都要调用 chabge-vars 函数。</p><p>现在只剩下定义用以证明复杂表达式的函数了。下面就是需要的函数：</p><p>(defun prove-and (clauses binds)<br />&#160; (if (null clauses)<br />&#160; &#160; &#160; (list binds)<br />&#160; &#160; &#160; (mapcan #&#039;(lambda (b)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; (prove (car clauses) b))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; (prove-and (cdr clauses) binds))))</p><p>(defun prove-or (clauses binds)<br />&#160; (mapcan #&#039;(lambda (c) (prove c binds))<br />&#160; &#160; &#160; &#160; &#160; clauses))</p><p>(defun prove-not (clause binds)<br />&#160; (unless (prove clause binds)<br />&#160; &#160; (list binds)))</p><p>图 15.4 逻辑操作符 (Logical operators)</p><p>操作一个 or 或者 not 表达式是非常简单的。操作 or 时，我们提取在 or 之间的每一个表达式返回的绑定。操作 not 时，当且仅当在 not 里的表达式产生 none 时，返回当前的绑定。</p><p>prove-and 函数稍微复杂一点。它像一个过滤器，它用之后的表达式所建立的每一个绑定来证明第一个表达式。这将导致 and 里的表达式以相反的顺序被求值。除非调用 prove 中的 prove-and 函数则会先逆转它们。</p><p>现在我们有了一个可以工作的程序，但它不是很友好。必须要解析 prove-and 返回的绑定列表是令人厌烦的，它们会变得更长随着规则变得更加复杂。下面有一个宏来帮助我们更愉快地使用这个程序：</p><p>(defmacro with-answer (query &amp;body body)<br />&#160; (let ((binds (gensym)))<br />&#160; &#160; `(dolist (,binds (prove &#039;,query))<br />&#160; &#160; &#160; &#160;(let ,(mapcar #&#039;(lambda (v)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;`(,v (binding &#039;,v ,binds)))<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(vars-in query))<br />&#160; &#160; &#160; &#160; &#160;,@body))))</p><p>图 15.5 介面宏 (Interface macro)</p><p>它接受一个 query （不被求值）和若干表达式构成的 body 作为参数，把 query 所生成的每一组绑定的值赋给 query 中对应的模式变量，并计算 body 。</p><p>&gt; (with-answer (parent ?x ?y)<br />&#160; &#160; (format t &quot;~A is the parent of ~A.~%&quot; ?x ?y))<br />DONALD is the parent of NANCY.<br />NIL</p><p>这个宏帮我们做了解析绑定的工作，同时为我们在程序中使用 prove 提供了一个便捷的方法。下面是这个宏展开的情况：</p><p>(with-answer (p ?x ?y)<br />&#160; (f ?x ?y))</p><p>;;将被展开成下面的代码</p><p>(dolist (#:g1 (prove &#039;(p ?x ?y)))<br />&#160; (let ((?x (binding &#039;?x #:g1))<br />&#160; &#160; &#160; &#160; (?y (binding &#039;?y #:g1)))<br />&#160; &#160; (f ?x ?y)))</p><p>图 15.6: with-answer 调用的展开式</p><p>下面是使用它的一个例子：</p><p>(&lt;- (parent donald nancy))<br />(&lt;- (parent donald debbie))<br />(&lt;- (male donald))<br />(&lt;- (father ?x ?y) (and (parent ?x ?y) (male ?x)))<br />(&lt;- (= ?x ?y))<br />(&lt;- (sibling ?x ?y) (and (parent ?z ?x)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(parent ?z ?y)<br />&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160;(not (= ?x ?y))))</p><p>;;我们可以像下面这样做出推论</p><p>&gt; (with-answer (father ?x ?y)<br />&#160; &#160; (format t &quot;~A is the father of ~A.~%&quot; ?x ?y))<br />DONALD is the father of DEBBIE.<br />DONALD is the father of NANCY.<br />NIL<br />&gt; (with-answer (sibling ?x ?y))<br />&#160; &#160; (format t &quot;~A is the sibling of ~A.~%&quot; ?x ?y))<br />DEBBLE is the sibling of NANCY.<br />NANCY is the&#160; sibling of DEBBIE.<br />NIL</p><p>图 15.7: 使用中的程序<br />15.4 分析 (Analysis)</p><p>看上去，我们在这一章中写的代码，是用简单自然的方式去实现这样一个程序。事实上，它的效率非常差。我们在这里是其实是做了一个解释器。我们能够把这个程序做得像一个编译器。</p><p>这里做一个简单的描述。基本的思想是把整个程序打包到两个宏 &lt;- 和 with-answer ，把已有程序中在运行期做的多数工作搬到宏展开期（在 10.7 节的 avg 可以看到这种构思的雏形) 用函数取代列表来表示规则，我们不在运行时用 prove 和 prove-and 这样的函数来解释表达式，而是用相应的函数把表达式转化成代码。当一个规则被定义的时候就有表达式可用。为什么要等到使用的时候才去分析它呢？这同样适用于和 &lt;- 调用了相同的函数来进行宏展开的 with-answer 。</p><p>听上去好像比我们已经写的这个程序复杂很多，但其实可能只是长了两三倍。想要学习这种技术的读者可以看 On Lisp 或者Paradigms of Artificial Intelligence Programming ，这两本书有一些使用这种风格写的示例程序。</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Fri, 18 Nov 2022 13:14:49 +0000</pubDate>
			<guid>https://www.gentoo-zh.org/viewtopic.php?pid=621#p621</guid>
		</item>
	</channel>
</rss>
