4.4.4 实现查询系统
4.4.2部分描述了查询系统如何工作。现在我们通过表示出系统的一个完整的实现,来填充它的细节。
4.4.4.1 驱动循环与实例化
查询系统的驱动循环重复地读取输入的表达式。如果表达式是一个规则或者是断言,
就被添加到数据库中,然后信息就加上了。否则,表达式被作为一个查询来对待。
驱动把这个查询传给解释器qeval,再结合初始化的帧的流,这个流包括了一个单独的空的帧。
解释的结果是通过满足的查询并且带着从数据库中找到的变量的值生成的帧的流。
这些帧被用来生成一个新的流,它由原始的查询,并且它的变量实例化的副本组成。
这个最终的流被打印在终端上。
(define input-prompt ";;; Query input:")
(define output-prompt ";;; Query result:")
(define (query-driver-loop)
(prompt-for-input input-prompt)
(let ((q (query-syntax-process (read))))
(cond ((assertion-to-be-add? q)
(add-rule-or-assertion!)
(newline)
(display "Assertion added to database.")
(query-driver-loop)
)
(else
(newline)
(display output-prompt)
(display-stream
(stream-map
(lambda (frame)
(instantiate q
frame
(lambda (v f) (contract-question-mark v))
)
)
(qeval q (singleton-stream '()))
))
(query-driver-loop)
)
)
)
)
这里,作为在这一章里的另一个解释器,我们为了查询语言的表达式,
使用了一个抽象的语法。表达式语法的实现,包括在4.4.4.7部分中给出的
判断式 assertion-to-be-added?和选择子 add-assertion-body. 在4.4.4.5部分中
定义了 add-rule-or-assertion!
在一个输入的表达式上做任何的处理之前,驱动循环为了让处理过程更加高效,
把它做了语法上的形式转换。这包括了改变了模式变量的表示形式。当查询被实例化时,
在被打印之前,任何未被绑定的变量都被转换回输入时的表示形式。这些转换被4.4.4.7
部分中的query-syntax-process 和 contract-question-mark程序执行。
为了实例化一个表达式,我们复制它,在表达式中用它们的在一个给定的帧的值来替换变量。
值本身被实例化,因为它能包括变量(例如,如果在exp中的?x 被绑定到?y 作为一致性的
结果,?y绑定到5)如果一个变量不能被实例化,采用的动作由Instantiate的一个程序化的参数给定。
(define (instantiate exp frame unbound-var-handler)
(define (copy exp)
(cond ((var? exp)
(let ((binding (binding-in-frame exp frame)))
(if binding
(copy (binding-value binding))
(unbound-var-handler exp frame))))
((pair? exp)
(cons (copy (car exp)) (copy (cdr exp))))
(else exp)))
(copy exp))
在4.4.4.8部分中定义了操纵绑定的程序。
4.4.4.2 解释器
qeval 程序被query-driver-loop 程序调用,是一个查询系统的基本的解释器。它以一个查询和
一个帧的流为实际参数,并且它返回一个扩展的帧的流。它标识着关键字通过一个使用得到与放
操作的数据驱动的分发,正如在第二章中实现通用的操作时我们所做的那样。任何的查询没有被识别
为一个关键字,就被假定为一个简单的查询, 由 simple-query程序来处理。
(define (qeval query frame-stream)
(let ((qproc (get (type query) 'qeval)))
(if qproc
(qproc (contents query) frame-stream)
(simple-query query frame-stream))))
类型和内容被定义在4.4.4.7部分中,实现了关键字的抽象的语法。
*简单查询
simple-query程序来处理简单的查询。它以一个简单的查询和一个帧的流为实际参数,返回扩展的帧
形成的流,通过查询匹配的所有的数据。
(define (simple-query query-pattern frame-stream)
(stream-flatmap
(lambda (frame)
(stream-append-delayed
(find-assertions query-pattern frame)
(delay (apply-rules query-pattern frame))))
frame-stream))
对于输入流中的每一个帧,我们使用 找到断言(见4.4.4.3部分中的find-assertions) 来匹配
符合模式的数据库中所有的断言,生成一个扩展的帧的流,并且我们使用 应用规则
(见4.4.4.4部分中的 apply-rules)来应用所有的可能的规则,生成扩展的帧的另一个流。这两个流被组合
(使用stream-append-delayed 见4.4.4.6部分)来生成一个流来保证给定的模型能够被满足
并且与原来的帧保持一致。输入的帧的流被合并使用stream-flatmap(见4.4.4.6部分)来形成一个大的流,
原来的输入的流的任何的帧能被扩展,来生成对给定的模式的匹配。
*复合查询
与查询 被程序 conjoin处理,正如图4.5中的演示。conjoin以输入的联合和帧的流为实际参数,返回扩展的帧
的流。首先,conjoin处理帧的流来找到所有的可能的帧的扩展来满足联合中的第一个查询的流。
然后,使用这个作为一个新的帧的流,它递归地应用conjoin来联合查询的其它部分。
(define (conjoin conjuncts frame-stream)
(if (empty-conjunction? conjuncts)
frame-stream
(conjoin (rest-conjuncts conjuncts)
(qeval (first-conjunct conjuncts)
frame-stream))))
表达式(put 'and 'qeval conjoin)在遇到了一个与表达式时,设置qeval 分发到conjoin。
或查询的处理与之相似,显示在图4.6中。或的各个部分的输出流被单独地计算并且使用
interleave-delayed程序(来自于4.4.4.6部分)来合并。
(define (disjoin disjuncts frame-stream)
(if (empty-disjunction? disjuncts)
the-empty-stream
(interleave-delayed
(qeval (first-disjunct disjuncts) frame-stream)
(delay (disjoin (rest-disjuncts disjuncts)
frame-stream)))))
(put 'or 'qeval disjoin)
与和或的语法的判断式和选择子在4.4.4.7部分中给出来了。
*过滤器
非的处理由4.4.2部分中列出的方法来处理。我们试着扩展输入流中的每个帧来满足否定的查询,
并且,如果它不能被扩展的话,我们在输出流中仅包括了一个给定的帧。
(define (negate operands frame-stream)
(stream-flatmap
(lambda (frame)
(if (stream-null? (qeval (negated-query operands)
(singleton-stream frame)))
(singleton-stream frame)
the-empty-stream))
frame-stream))
(put 'not 'qeval negate)
lisp-value是一个过滤器与非相似。在流中的每个帧被用来实例化模式中的变量,
显示的判断式被应用,判断式的返回假的帧被过滤出输入流。如果有未绑定的变量
有一个出错的结果。
(define (lisp-value call frame-stream)
(stream-flatmap
(lambda (frame)
(if (execute
(instantiate
call
frame
(lambda (v f)
(error "Unknown pat var -- LISP-VALUE" v))))
(singleton-stream frame)
the-empty-stream))
frame-stream))
(put 'lisp-value 'qeval lisp-value)
Execute把判断式应用到它的实际参数上,必须解释判断式的表达式来得到要应用的
程序。然而,它不是必须解释实际参数,因为它们已经是实际参数了,不是表达式将
生成实际参数。注意的是,execute使用eval和apply从底层的lisp系统实现了。
(define (execute exp)
(apply (eval (predicate exp) user-initial-environment)
(args exp)))
always-true关键字提供了一个总是被满足的查询。它忽略它的内容(正常是空的)
和简单地传递于输入流中的所有的帧。rule-body选择子使用always-true提供了
被定义好的规则体,没有规则体。(也就是,规则的结论总是被满足)
(define (always-true ignore frame-stream) frame-stream)
(put 'always-true 'qeval always-true)
选择子定义的not和lisp-value的语法在4.4.4.7部分中给出了。
4.4.4.3 通过模式匹配找到断言
(在4.4.4.2部分中)simple-query调用了find-assertion,它有一个输入模式和一个帧为参数,
它返回一个帧的流,用匹配给定的模式数据库扩展每个帧。它使用fetch-assertions(在4.4.4.5部分中)
得到一个数据库中的应该被检查满足一个模式的匹配的所有的断言的流。这里对于fetch-assertions的原因
是我们能够经常应用简单的测试从一个成功的匹配的候选者的池来消除数据库的许多的记录。如果我们消除了
fetch-assertion,这个系统仍然能工作,并且简单检查数据库中的所有的断言的一个流,但是计算可能没有效率
因为我们做对匹配器的更多次的调用。
(define (find-assertions pattern frame)
(stream-flatmap (lambda (datum)
(check-an-assertion datum pattern frame))
(fetch-assertions pattern frame)))
check-an-assertion以一个模式,一个数据对象,一个帧为参数,返回一个元素的包括了扩展帧的流
或者是如果匹配失败的话,是一个空的流。
(define (check-an-assertion assertion query-pat query-frame)
(let ((match-result
(pattern-match query-pat assertion query-frame)))
(if (eq? match-result 'failed)
the-empty-stream
(singleton-stream match-result))))
基本的模式匹配器返回符号failed或者是一个给定的帧的扩展。匹配器的基本的思想是
检查模式与数据的匹配,一个元素接着一个元素,为模式变量累加绑定。如果模式和
数据对象是一致的,匹配成功,我们返回绑定被累加的帧。否则如果模式是一个变量,
通过绑定变量到数据,我们扩展当前的帧,只要这与帧中的已有的绑定是具有一致性的。
如果模式和数据都是数对,我们递归地匹配模式的头部和数据的头部,来生成一个帧,
在这个帧中,我们然后匹配模式的尾部和数据的尾部。如果这些情况没有可用的,匹配
失败了,我们返回符号fails.
(define (pattern-match pat dat frame)
(cond ((eq? frame 'failed) 'failed)
((equal? pat dat) frame)
((var? pat) (extend-if-consistent pat dat frame))
((and (pair? pat) (pair? dat))
(pattern-match (cdr pat)
(cdr dat)
(pattern-match (car pat)
(car dat)
frame)))
(else 'failed)))
这是通过添加一个新的绑定来扩展一个帧的程序,如果这和帧中的已有的绑定有一致性的话。
(define (extend-if-consistent var dat frame)
(let ((binding (binding-in-frame var frame)))
(if binding
(pattern-match (binding-value binding) dat frame)
(extend var dat frame))))
如果在帧中的变量没有绑定,我们简单地加上变量的绑定到数据中。
否则我们匹配,在帧中的变量的值。如果存储的值仅包含了常数,正如它必须
是在模式匹配期间由extend-if-consistent存储的,然后匹配简单地测试存储的
值与新的值是否是相同的。如果相同,它返回未修改的帧;如果不同,它返回
一个失败的显示。存储的模式与新的数据的递归的匹配将在这个模式中为变量
添加或者是检查绑定情况。例如,假定我们有一个帧,它的?x被绑定为(f ?y)和
?y 未绑定,我们要实例化这个帧,以x到(f b)的绑定。我们查找?x并且发现它绑定
为(f ?y). 这导致了我们要匹配(f ?y)和假定的新值(f b)在相同的帧中。最终
这个匹配以添加了?y到b的绑定而扩展了这个帧。我们根本没有修改一个已存储的绑定,
我们没有存储一个给定的变量的超过一个的绑定。
程序用extend-if-consistent来操纵在4.4.4.8部分中被定义的绑定。
* 有点结尾的模式
如果一个模式包括了一个点,后跟着一个模式变量,模式变量匹配数据列表的其它部分
(而不是数据列表的下一个元素),仅有一种情况除外,就是在练习2.20中描述的以
点结尾的标识。尽管我们已经实现的模式匹配器没有查找点,它的行为正如 我们所要的。
这是因为lisp的读原生程序,它被query-driver-loop程序用来读查询,并且把它表示为
一个列表结构,处理点以一个特殊的方式。
当读看到一个点时,代替让下一项成列表的下一个元素,它让下一项成为列表的其它的部分。
例如,读为了模式(computer ?type)生成的列表结构,通过解释表达式(cons 'computer (cons '?type '()))
能被组装,并且通过解释表达式 (cons 'computer '?type) 能组装 (computer . ?type).
因此,正如pattern-match递归地比较一个数据列表的头部和尾部,一个模式有一个点,它最终匹配
点后的变量和数据列表的一个子列表,绑定变量到那个列表。例如,匹配模式(computer . ?type)
和数据 (computer programmer trainee) 将把?type匹配成列表(programmer trainee).