# 4.4.2查询系统如何工作

> 译者： [https://sicp.comp.nus.edu.sg/chapters/91](https://sicp.comp.nus.edu.sg/chapters/91)

在 [4.4.4](93) 部分中，我们将查询解释器的实现作为功能集合提供。 在本节中，我们给出了概述，其解释了与底层实现细节无关的系统的一般结构。 在描述了解释器的实现之后，我们将能够理解它的某些局限性以及查询语言的逻辑运算不同于数学逻辑运算的一些微妙方式。

显然，查询评估器必须执行某种搜索，以便将查询与数据库中的事实和规则进行匹配。 一种方法是使用 [4.3](85)[4.69](93#ex_4.69) ）。 另一种可能性是借助于流来管理搜索。 我们的实现遵循第二种方法。

查询系统是围绕两个称为_模式匹配_和_统一_的中心操作组织的。 我们首先描述模式匹配，并说明此操作以及如何根据帧流组织信息，使我们能够实现简单查询和复合查询。 接下来，我们讨论统一，这是实施规则所需的模式匹配的概括。 最后，我们展示如何通过一个函数对整个查询解释器进行拟合，该函数以类似于 [4.1](73) 中所述的`evaluate`为解释器分类表达式的方式进行分类。

## 模式匹配

_模式匹配器_是用于测试某些数据是否适合指定模式的程序。 例如，数据列表`list(list("a", "b"), "c", list("a", "b"))`将模式`list(x, "c", x)`与绑定到`list("a", "b")`的模式变量`x`进行匹配。 相同的数据列表将模式`list(x, y, z)`与`x`和`z`都绑定到`list("a", "b")`并将`y`绑定到`"c"`进行匹配。 它还将模式`list(list(x, y), "c", list(x, y))`与绑定到`"a"`的`x`和绑定到`"b"`的`y`进行匹配。 但是，它与模式`list(x, "a", y)`不匹配，因为该模式指定了第二个元素为字符串`"a"`的列表。

查询系统使用的模式匹配器将模式，数据和指定各种模式变量绑定的_帧_作为输入。 它以与框架中已有绑定一致的方式检查数据是否与模式匹配。 如果是这样，它将返回由匹配可能已确定的任何绑定增强的给定帧。 否则，表明匹配失败。

例如，在给定一个空帧的情况下，使用模式`list(x, y, z)`匹配`list("a", "b", "c")`将返回一个帧，该帧指定`x`绑定到`"a"`而`y`绑定到`"b"`。 尝试使用相同的图案，相同的基准和指定`y`绑定到`"a"`的帧进行匹配将失败。 尝试以相同的模式，相同的基准以及未绑定`y`到`b`和`x`的帧的匹配，将返回通过将`x`绑定到`"a"`来增强的给定帧。

模式匹配器是处理不涉及规则的简单查询所需的所有机制。 例如，处理查询

```js
job(x, list("computer", "programmer"));
```

we scan through all assertions in the data base and select those that match the pattern with respect to an initially empty frame. For each match we find, we use the frame returned by the match to instantiate the pattern with a value for `x`.

## 帧流

通过使用流来组织针对帧的模式测试。 给定单个帧，匹配过程将逐个遍历数据库条目。 对于每个数据库条目，匹配器将生成一个特殊符号（表示匹配失败）或扩展到框架。 所有数据库条目的结果都收集到一个流中，该流通过过滤器清除故障。 结果是所有帧的流，这些流通过匹配数据库中的某个断言来扩展给定帧。 [[1]](91#footnote-1)

在我们的系统中，查询获取输入的帧流，并对流中的每个帧执行上述匹配操作，如图 [4.4](91#fig_4.4) 所示。 即，对于输入流中的每个帧，查询将通过匹配数据库中的断言来生成一个新流，该流包括对该帧的所有扩展。 然后将所有这些流组合在一起，形成一个巨大的流，其中包含输入流中每个帧的所有可能扩展。 此流是查询的输出。

<figure>![](img/6a0582682f67678dabf9e0a038ccbb00.jpg)**[图4.4](91#fig_4.4)** 查询处理帧流。</figure>

为了回答一个简单的查询，我们将查询与包含单个空框架的输入流一起使用。 结果输出流包含对空框架的所有扩展（即，对我们查询的所有答案）。 然后，使用该帧流生成原始查询模式的副本流，其中每个帧中的值实例化了变量，这就是最终打印出的流。

## 复合查询

当我们处理复合查询时，框架流实现的真正优雅显而易见。 复合查询的处理利用了我们的匹配器要求匹配与指定框架保持一致的能力。 例如，要处理两个查询的`and`，例如

```js
and(can_do_job(x, list("computer", "programmer", "trainee")),
    job(person, x))
```

(informally, <quote>Find all people who can do the job of a computer programmer trainee</quote>), we first find all entries that match the pattern

```js
can_do_job(x, list("computer", "programmer", "trainee"))
```

这将产生一个帧流，每个帧都包含`x`的绑定。 然后，对于流中的每个帧，我们找到所有匹配的条目

```js
job(person, x)
```

in a way that is consistent with the given binding for `x`. Each such match will produce a frame containing bindings for `x` and `person`. The `and` of two queries can be viewed as a series combination of the two component queries, as shown in figure <ref name="fig:query-and">[4.5](91#fig_4.5)</ref>. The frames that pass through the first query filter are filtered and further extended by the second query. <split><figure>![](img/53a5854b1a08a7bed3cc3a23ed8ce2e9.jpg)**[Figure 4.5](91#fig_4.5)** The `and` combination of two queries is produced by operating on the stream of frames in series.</figure></split> 

图 [4.6](91#fig_4.6) 显示了用于计算两个查询的`or`作为两个组件查询的并行组合的类似方法。 每个查询分别扩展输入帧的流。 然后将两个结果流合并以产生最终输出流。

<figure>![](img/0fcc1ade2b120b40b0f377c0ea8feb91.jpg)**[图4.6](91#fig_4.6)** 通过并行处理帧流并将结果合并，可以生成两个查询的`or`组合。</figure>

即使从这种高级描述，很明显复合查询的处理也可能很慢。 例如，由于一个查询可能为每个输入帧产生一个以上的输出帧，并且`and`中的每个查询都从先前的查询中获取其输入帧，因此`and`查询在最坏的情况下可能必须执行 在查询数量中呈指数形式的匹配数（请参阅练习 [4.67](93#ex_4.67) ）。 [[2]](91#footnote-2) 尽管仅用于处理简单查询的系统非常实用，但是处理复杂查询却非常困难。 [[3]](91#footnote-3)

从帧流的角度来看，某些查询的`not`充当过滤器，该过滤器删除了所有可以满足查询条件的帧。 例如，给定模式

```js
not(job(x, list("computer", "programmer")))
```

we attempt, for each frame in the input stream, to produce extension frames that satisfy `job(x, list("computer", "programmer"))`. We remove from the input stream all frames for which such extensions exist. The result is a stream consisting of only those frames in which the binding for `x` does not satisfy `job(x, list("computer", "programmer"))`. For example, in processing the query

```js
and(supervisor(x, y),
    not(job(x, list("computer", "programmer"))));
```

the first clause will generate frames with bindings for `x` and `y`. The `not` clause will then filter these by removing all frames in which the binding for `x` satisfies the restriction that `x` is a computer programmer.[[4]](91#footnote-4)

`javascript-value`表达式被实现为帧流上的类似过滤器。 我们使用流中的每一帧实例化模式中的任何变量，然后应用JavaScript谓词。 我们从输入流中删除谓词失败的所有帧。

## 统合

为了使用查询语言处理规则，我们必须能够找到结论与给定查询模式匹配的规则。 规则结论就像断言一样，除了它们可以包含变量之外，因此我们需要模式匹配的概括（称为_统一_），其中&lt;quote&gt;模式&lt;/quote&gt;和&lt;quote&gt;数据&lt;/quote&gt;可能包含变量。

统一器采用两个模式，每个模式都包含常量和变量，并确定是否有可能为将两个模式相等的变量赋值。 如果是这样，它将返回包含这些绑定的框架。 例如，统一`list(x, "a", y)`和`list(y, z, "a")`将指定其中`x`，`y`和`z`必须都绑定到`"a"`的帧。 另一方面，统一`list(x, y, "a")`和`list(x, "b", y)`将会失败，因为`y`没有使两个模式相等的值。 （要使模式的第二个元素相等，`y`必须为`"b"`；但是，要使第三个元素相等，`y`必须为`"a"`。） 像模式匹配器一样，查询系统将一个框架作为输入并执行与此框架一致的统一。

统一算法是查询系统中技术上最困难的部分。 对于复杂的模式，执行统一似乎需要推论。 例如，为了统一`list(x, x)`和`list(list("a", y, "c"), list("a", "b", z))`，该算法必须推断`x`应该是`list("a", "b", "c")`，`y`应该是`"b"`和`z`应该是`"c"`。 我们可能认为此过程是解决模式组件之间的一组方程式。 通常，这些是联立方程，可能需要大量操作才能求解。 [[5]](91#footnote-5) 例如，将`list(x, x)`和`list(list("a", y, "c"), list("a", "b", z))`统一可以认为是指定联立方程`x ![%3D](img/f7ef8d7b340ed17969bfbde43f695928.jpg) list("a", y, "c") x ![%3D](img/f7ef8d7b340ed17969bfbde43f695928.jpg) list("a", "b", z)`

这些方程式表示`list("a", y, "c") ![%3D](img/f7ef8d7b340ed17969bfbde43f695928.jpg) list("a", "b", z)`，这又意味着`"a" ![%3D](img/f7ef8d7b340ed17969bfbde43f695928.jpg) "a", y ![%3D](img/f7ef8d7b340ed17969bfbde43f695928.jpg) "b", "c" ![%3D](img/f7ef8d7b340ed17969bfbde43f695928.jpg) z,`，因此也意味着`x ![%3D](img/f7ef8d7b340ed17969bfbde43f695928.jpg) list("a", "b", "c")`

在成功的模式匹配中，所有模式变量都将被绑定，并且绑定到它们的值仅包含常量。 到目前为止，我们看到的所有统一示例都是如此。 但是，总的来说，成功的统一可能无法完全确定变量值。 一些变量可能会保持未绑定状态，而另一些变量可能会绑定到包含变量的值。

考虑`list(x, "a")`和`list(list("b", y), z)`的统一。 我们可以推断出`x` ![%3D](img/f7ef8d7b340ed17969bfbde43f695928.jpg) `list("b", y)`和`"a"` ![%3D](img/f7ef8d7b340ed17969bfbde43f695928.jpg) `z`，但我们无法进一步求解`x`或`y`。 统一不会失败，因为通过将值分配给`x`和`y`当然可以使两个模式相等。 由于此匹配绝不会限制`y`的取值，因此`y`的绑定不会放入结果框中。 但是，该匹配确实限制了`x`的值。 无论`y`的值如何，`x`都必须为`list("b", y)`。 因此，将`x`与图案`list("b", y)`的绑定放入帧中。 如果稍后确定`y`的值并将其添加到帧中（通过与该帧一致所需的模式匹配或统一），则先前绑定的`x`将引用该值。 [ [6]](91#footnote-6)

## 适用规则

统一是查询系统组件的关键，该组件可以从规则进行推断。 要查看此操作是如何完成的，请考虑处理涉及应用规则的查询，例如

```js
lives_near(x, list("Hacker", "Alyssa", "P"));
```

要处理此查询，我们首先使用上面描述的普通模式匹配功能来查看数据库中是否有与此模式匹配的声明。 （在这种情况下不会有任何结果，因为我们的数据库不包含有关谁住在谁附近的直接断言。）下一步是尝试通过每个规则的结论统一查询模式。 我们发现模式与规则的结论相统一

```js
rule(lives_near(person_1, person_2),
     and(address(person_1, pair(town, rest_1)),
         address(person_2, list(town, rest_2)),
         not(same(person_1, person_2))));
```

resulting in a frame specifying that `person_2` is bound to `list("Hacker", "Alyssa", "P")` and that `x` should be bound to (have the same value as) `person_1`. Now, relative to this frame, we evaluate the compound query given by the body of the rule. Successful matches will extend this frame by providing a binding for `person_1`, and consequently a value for `x`, which we can use to instantiate the original query pattern.

通常，当尝试在为某些模式变量指定绑定的框架中建立查询模式时，查询评估器使用以下方法来应用规则：

*   如果成功，则将查询与规则的结论统一起来，以形成原始框架的扩展。
*   相对于扩展框架，评估由规则主体形成的查询。

请注意，这与在JavaScript的`evaluate` / `apply`评估器中应用函数的方法有多相似：

*   将函数的参数绑定到其参数以形成扩展原始函数环境的框架。
*   相对于扩展环境，评估由函数主体形成的表达式。

两位评估者之间的相似性不足为奇。 就像函数定义是JavaScript中的抽象手段一样，规则定义也是查询语言中的抽象手段。 在每种情况下，我们都通过创建适当的绑定并评估相对于它们的规则或函数主体来展开抽象。

## 简单查询

我们在本节的前面部分看到了如何在没有规则的情况下评估简单查询。 现在我们已经了解了如何应用规则，我们可以描述如何通过使用规则和断言来评估简单查询。

给定查询模式和帧流，我们为输入流中的每个帧生成两个流：

*   通过将模式与数据库中的所有断言进行匹配而获得的扩展帧流（使用模式匹配器），以及
*   通过应用所有可能的规则（使用统一器）获得的扩展帧流。 [[7]](91#footnote-7)

追加这两个流会生成一个流，该流由可以满足给定模式与原始帧的所有方式组成。 这些流（输入流中的每个帧对应一个流）现在全部组合在一起，形成一个大流，因此，它包含了可以扩展原始输入流中的任何帧以产生与给定模式匹配的所有方式。 。

## 查询评估器和驱动程序循环

尽管基础匹配操作很复杂，但系统的组织方式非常类似于任何语言的评估器。 协调匹配操作的函数称为`qeval`，其作用类似于JavaScript的`evaluate`函数。 函数`qeval`将查询和帧流作为输入。 它的输出是一个帧流，对应于与查询模式的成功匹配，这些帧扩展了输入流中的某些帧，如图 [4.4](91#fig_4.4) 所示。 像`evaluate`一样，`qeval`对表达式（查询）的不同类型进行分类，并为每个表达式分配适当的函数。 每种特殊形式（`and`，`or`，`not`和`javascript_value`）都有一个功能，一个用于简单查询。

与本章其他评估程序的`driver_loop`功能类似，驱动程序循环从终端读取查询。 对于每个查询，它使用查询和由单个空框架组成的流调用`qeval`。 这将产生所有可能匹配的流（对空框架的所有可能扩展）。 对于结果流中的每个帧，它使用帧中找到的变量的值实例化原始查询。 然后打印此实例化查询流。 [[8]](91#footnote-8)

驱动程序还会检查特殊命令`assert`，该命令指示输入不是查询，而是要添加到数据库的声明或规则。 例如，

```js
assert(job(list("Bitdiddle", "Ben"), list("computer", "wizard")));

assert(rule(wheel(person),
            and(supervisor(middle-manager, person),
                supervisor(x, middle-manager))));
```

* * *

[[1]](91#footnote-link-1) Because matching is generally very expensive, we would like to avoid applying the full matcher to every element of the data base. This is usually arranged by breaking up the process into a fast, coarse match and the final match. The coarse match filters the data base to produce a small set of candidates for the final match. With care, we can arrange our data base so that some of the work of coarse matching can be done when the data base is constructed rather then when we want to select the candidates. This is called _indexing_ the data base. There is a vast technology built around data-base-indexing schemes. Our implementation, described in section <ref name="sec:implementing-the-query-system">[4.4.4](93)</ref>, contains a simple-minded form of such an optimization.

[[2]](91#footnote-link-2) But this kind of exponential explosion is not common in `and` queries because the added conditions tend to reduce rather than expand the number of frames produced.

[[3]](91#footnote-link-3) There is a large literature on data-base-management systems that is concerned with how to handle complex queries efficiently.

[[4]](91#footnote-link-4) There is a subtle difference between this filter implementation of `not` and the usual meaning of `not` in mathematical logic. See section <ref name="sec:math-logic">[4.4.3](92)</ref>.

[[5]](91#footnote-link-5) In one-sided pattern matching, all the equations that contain pattern variables are explicit and already solved for the unknown (the pattern variable).

[[6]](91#footnote-link-6) Another way to think of unification is that it generates the most general pattern that is a specialization of the two input patterns. That is, the unification of `list(x, "a")` and `list(list("b", y), z)` is `list(list("b", y), "a")`, and the unification of `list(x, "a", y)` and `list(y, z, "a")`, discussed above, is `list("a", "a", "a")`. For our implementation, it is more convenient to think of the result of unification as a frame rather than a pattern.

[[7]](91#footnote-link-7) Since unification is a generalization of matching, we could simplify the system by using the unifier to produce both streams. Treating the easy case with the simple matcher, however, illustrates how matching (as opposed to full-blown unification) can be useful in its own right.

[[8]](91#footnote-link-8) The reason we use streams (rather than lists) of frames is that the recursive application of rules can generate infinite numbers of values that satisfy a query. The delayed evaluation embodied in streams is crucial here: The system will print responses one by one as they are generated, regardless of whether there are a finite or infinite number of responses.

