NUS CS1101S:SICP JavaScript 描述:四、元语言抽象(下)

30 阅读58分钟

4.3.3 实现amb求值器

普通 JavaScript 程序的求值可能返回一个值,可能永远不会终止,也可能会发出错误。在非确定性 JavaScript 中,程序的求值可能会导致发现死胡同,此时求值必须回溯到先前的选择点。这种额外情况使得非确定性 JavaScript 的解释变得复杂。

我们将通过修改第 4.1.7 节的分析求值器来构建非确定性 JavaScript 的amb求值器。与分析求值器一样,组件的求值是通过调用分析该组件的执行函数来完成的。普通 JavaScript 的解释和非确定性 JavaScript 的解释之间的区别将完全体现在执行函数中。

执行函数和延续

请记住,普通求值器的执行函数接受一个参数:执行环境。相比之下,amb求值器中的执行函数接受三个参数:环境和两个称为延续函数的函数。组件的求值将通过调用这两个延续函数之一来完成:如果求值得到一个值,将调用成功延续并传递该值;如果求值导致发现死胡同,则调用失败延续。构造和调用适当的延续是非确定性求值器实现回溯的机制。

成功延续的工作是接收一个值并继续计算。除了该值之外,成功延续还传递另一个失败延续,如果使用该值导致死胡同,随后将调用该失败延续。

失败延续的工作是尝试非确定性过程的另一个分支。非确定性语言的本质在于组件可以代表在选择之间进行选择。这样的组件的求值必须继续进行所指示的备选选择之一,即使事先不知道哪些选择将导致可接受的结果。为了处理这个问题,求值器选择其中一个备选方案,并将该值传递给成功延续。除了该值之外,求值器还构造并传递一个失败延续,以便稍后调用以选择不同的备选方案。

在求值过程中触发失败(即调用失败延续)当用户程序明确拒绝当前攻击线路时(例如,调用require可能导致执行amb(),一个总是失败的表达式-见第 4.3.1 节)。此时手头的失败延续将导致最近的选择点选择另一个替代方案。如果在该选择点没有更多的替代方案需要考虑,将触发较早选择点的失败,依此类推。失败延续还会在驱动循环响应retry请求时调用,以找到程序的另一个值。

此外,如果在选择的过程中发生了副作用操作(例如对变量的赋值),当过程找到死胡同时,可能需要在进行新的选择之前撤消副作用。这是通过使副作用操作产生一个失败延续来实现的,该失败延续撤消副作用并传播失败。

总之,失败延续是由构建的

  • amb表达式-提供一种机制,如果amb表达式的当前选择导致死胡同,则进行替代选择。

  • 顶层驱动程序-提供一种机制,在选择耗尽时报告失败;

  • 分配-拦截失败并在回溯期间撤消分配。

只有在遇到死胡同时才会启动失败。这发生在

  • 如果用户程序执行amb()

  • 如果用户在顶层驱动程序处键入retry

在处理失败时也称为失败延续:

  • 当由分配创建的失败延续完成撤消副作用时,它调用它拦截的失败延续,以将失败传播回导致此分配的选择点或顶层。

  • amb的失败延续用尽选择时,它调用最初给amb的失败延续,以将失败传播回先前的选择点或顶层。

求值器的结构

amb求值器的语法和数据表示函数,以及基本的analyze函数,与第 4.1.7 节的求值器相同,只是我们需要额外的语法函数来识别amb语法形式:

function is_amb(component) {
    return is_tagged_list(component, "application") &&
           is_name(function_expression(component)) &&
           symbol_of_name(function_expression(component)) === "amb";
}
function amb_choices(component) {
    return arg_expressions(component);
}

我们继续使用第 4.1.2 节的解析函数,该函数不支持amb作为语法形式,而是将amb(``...``)视为函数应用。函数is_amb确保每当名称amb出现为应用的函数表达式时,求值器将“应用”视为不确定性选择点。

我们还必须在analyze的分发中添加一个子句,以识别这样的表达式并生成适当的执行函数:

...
: is_amb(component)
? analyze_amb(component)
: is_application(component)
...

顶层函数ambeval(类似于第 4.1.7 节中给出的evaluate版本)分析给定的组件,并将生成的执行函数应用于给定的环境,以及两个给定的延续:

function ambeval(component, env, succeed, fail) {
    return analyze(component)(env, succeed, fail);
}

成功延续是一个带有两个参数的函数:刚刚获得的值和另一个失败延续,如果该值导致后续失败,则使用该失败延续。失败延续是一个没有参数的函数。因此,执行函数的一般形式是

(env, succeed, fail) => {
    // succeed is (value, fail) => ...
    // fail is () => ...
    ...
}

例如,执行

ambeval(component,
        the_global_environment,
        (value, fail) => value,
        () => "failed");

将尝试求值给定的组件,并将返回组件的值(如果求值成功)或字符串"failed"(如果求值失败)。下面显示的驱动循环中对ambeval的调用使用了更复杂的延续函数,这些函数继续循环并支持retry请求。

amb求值器的大部分复杂性都来自于在执行函数相互调用时传递延续的机制。在阅读以下代码时,您应该将每个执行函数与第 4.1.7 节中给出的普通求值器的相应函数进行比较。

简单表达式

最简单类型表达式的执行函数基本上与普通求值器的执行函数相同,除了需要管理延续。执行函数只是成功地返回表达式的值,并传递给它们的失败延续。

function analyze_literal(component) {
    return (env, succeed, fail) =>
             succeed(literal_value(component), fail);
}

function analyze_name(component) {
    return (env, succeed, fail) =>
             succeed(lookup_symbol_value(symbol_of_name(component),
                                         env),
                    fail);
}

function analyze_lambda_expression(component) {
    const params = lambda_parameter_symbols(component);
    const bfun = analyze(lambda_body(component));
    return (env, succeed, fail) =>
             succeed(make_function(params, bfun, env),
                     fail);
}

请注意,查找名称总是“成功”的。如果lookup_symbol_value未能找到名称,它会像往常一样发出错误信号。这样的“失败”表示程序错误 - 引用未绑定的名称;这不是表明我们应该尝试另一个非确定性选择,而不是当前正在尝试的选择。

条件和序列

条件也以与普通求值器相似的方式处理。由analyze_conditional生成的执行函数调用谓词执行函数pfun,并使用一个成功延续来检查谓词值是否为真,并继续执行条件的结果或替代方案。如果pfun的执行失败,将调用条件表达式的原始失败延续。

function analyze_conditional(component) {
    const pfun = analyze(conditional_predicate(component));
    const cfun = analyze(conditional_consequent(component));
    const afun = analyze(conditional_alternative(component));
    return (env, succeed, fail) =>
             pfun(env,
                  // success continuation for evaluating the predicate
                  // to obtain pred_value (pred_value, fail2) =>
                    is_truthy(pred_value)
                    ? cfun(env, succeed, fail2)
                    : afun(env, succeed, fail2),
                  // failure continuation for evaluating the predicate
                  fail);
}

序列也以与以前的求值器相同的方式处理,除了在子函数sequentially中进行的操作,这些操作对于传递延续是必需的。即,为了依次执行ab,我们使用一个成功延续调用a,该成功延续调用b

function analyze_sequence(stmts) {
    function sequentially(a, b) {
        return (env, succeed, fail) =>
                 a(env,
                   // success continuation for calling a
                   (a_value, fail2) =>
                     is_return_value(a_value)
                     ? succeed(a_value, fail2)
                     : b(env, succeed, fail2),
                   // failure continuation for calling
                   a fail);
    }
    function loop(first_fun, rest_funs) {
        return is_null(rest_funs)
               ? first_fun
               : loop(sequentially(first_fun, head(rest_funs)),
                      tail(rest_funs));
    }
    const funs = map(analyze, stmts);
    return is_null(funs)
           ? env => undefined
           : loop(head(funs), tail(funs));
}
声明和赋值

声明是另一种情况,我们必须费力地管理延续,因为必须在实际声明新名称之前求值声明值表达式。为了实现这一点,使用环境、成功延续和失败延续调用声明值执行函数vfun。如果vfun的执行成功,获得了声明名称的值val,则声明名称并传播成功:

function analyze_declaration(component) {
    const symbol = declaration_symbol(component);
    const vfun = analyze(declaration_value_expression(component));
    return (env, succeed, fail) =>
             vfun(env,
                  (val, fail2) => {
                      assign_symbol_value(symbol, val, env);
                      return succeed(undefined, fail2);
                  },
                  fail);
}

赋值更有趣。这是我们真正使用延续的第一个地方,而不仅仅是传递它们。赋值的执行函数开始时与声明的执行函数类似。它首先尝试获取要分配给名称的新值。如果vfun的求值失败,赋值也失败。

然而,如果vfun成功,并且我们继续进行赋值,我们必须考虑这一计算分支可能以后会失败的可能性,这将需要我们回溯到赋值之外。因此,我们必须安排在回溯过程中撤消赋值。

这是通过给vfun一个成功继续(在下面标有注释1)来实现的,该成功继续在分配新值给变量并从分配中继续之前保存变量的旧值。与分配值一起传递的失败继续(在下面标有注释2)在继续失败之前恢复变量的旧值。也就是说,成功的分配提供了一个失败继续,该失败继续将拦截后续的失败;否则会调用fail2的任何失败都会调用此函数,以在实际调用fail2之前撤消分配。

function analyze_assignment(component) {
    const symbol = assignment_symbol(component);
    const vfun = analyze(assignment_value_expression(component));
    return (env, succeed, fail) =>
             vfun(env,
                  (val, fail2) => { // 1
                      const old_value = lookup_symbol_value(symbol,
                                                            env);
                      assign_symbol_value(symbol, val, env);
                      return succeed(val,
                                     () => { // 2
                                         assign_symbol_value(symbol,
                                                             old_value,
                                                             env);
                                         return fail2();
                                     });
                  },
                  fail);
}
返回语句和块

分析返回语句很简单。返回表达式被分析以产生执行函数。返回语句的执行函数调用具有成功继续的执行函数,该成功继续将返回值包装在返回值对象中并将其传递给原始成功继续。

function analyze_return_statement(component) {
    const rfun = analyze(return_expression(component));
    return (env, succeed, fail) =>
             rfun(env,
                  (val, fail2) =>
                    succeed(make_return_value(val), fail2),
                  fail);
}

块的执行函数在扩展环境上调用主体的执行函数,而不更改成功或失败继续。

function analyze_block(component) {
    const body = block_body(component);
    const locals = scan_out_declarations(body);
    const unassigneds = list_of_unassigned(locals);
    const bfun = analyze(body);
    return (env, succeed, fail) =>
             bfun(extend_environment(locals, unassigneds, env),
                  succeed,
                  fail);
}
函数应用

应用的执行函数中除了管理继续的技术复杂性外,没有新的想法。这种复杂性出现在analyze_ application中,因为我们在求值参数表达式时需要跟踪成功和失败继续。我们使用一个函数get_args来求值参数表达式的列表,而不是像普通求值器中那样简单地使用map

function analyze_application(component) {
    const ffun = analyze(function_expression(component));
    const afuns = map(analyze, arg_expressions(component));
    return (env, succeed, fail) =>
             ffun(env,
                  (fun, fail2) =>
                    get_args(afuns,
                             env,
                             (args, fail3) =>
                               execute_application(fun,
                                                   args,
                                                   succeed,
                                                   fail3),
                             fail2),
                  fail);
}

get_args中,注意如何通过使用一个递归调用get_args的成功继续来遍历afun执行函数列表并构造args的结果列表。每个对get_args的递归调用都有一个成功继续,其值是使用pair将新获得的参数添加到累积参数列表中得到的新列表:

function get_args(afuns, env, succeed, fail) {
    return is_null(afuns)
           ? succeed(null, fail)
           : head(afuns)(env,
                         // success continuation for this afun
                         (arg, fail2) =>
                           get_args(tail(afuns),
                                    env,
                                    // success continuation for
                                    // recursive call to get_args
                                    (args, fail3) =>
                                      succeed(pair(arg, args),
                                              fail3),
                                    fail2),
                         fail);
}

实际的函数应用是由execute_application执行的,与普通求值器一样,只是需要管理继续。

function execute_application(fun, args, succeed, fail) {
    return is_primitive_function(fun)
           ? succeed(apply_primitive_function(fun, args),
                     fail)
           : is_compound_function(fun)
           ? function_body(fun)(
                 extend_environment(function_parameters(fun),
                                    args,
                                    function_environment(fun)),
                 (body_result, fail2) =>
                   succeed(is_return_value(body_result)
                           ? return_value_content(body_result)
                           : undefined,
                           fail2),
                 fail)
           : error(fun, "unknown function type - execute_application");
}
求值**amb**表达式

amb语法形式是非确定性语言中的关键元素。在这里,我们看到了解释过程的本质以及跟踪继续的原因。amb的执行函数定义了一个循环try_next,该循环循环执行amb表达式的所有可能值的执行函数。每个执行函数都使用一个失败继续进行调用,该失败继续将尝试下一个值。当没有更多的替代方案可尝试时,整个amb表达式失败。

function analyze_amb(component) {
    const cfuns = map(analyze, amb_choices(component));
    return (env, succeed, fail) => {
               function try_next(choices) {
                   return is_null(choices)
                          ? fail()
                          : head(choices)(env,
                                          succeed,
                                          () =>
                                            try_next(tail(choices)));
               }
               return try_next(cfuns);
           };
}
驱动循环

由于允许用户重试求值程序的机制,amb求值器的驱动循环非常复杂。驱动程序使用一个名为internal_loop的函数,该函数以retry函数作为参数。意图是调用retry应该继续尝试非确定性求值中的下一个未尝试的替代方案。函数internal_loop要么在用户在驱动循环中键入retry时调用retry,要么通过调用ambeval开始新的求值。

对于对ambeval的此调用的失败继续通知用户没有更多的值,并重新调用驱动循环。

对于对ambeval的调用,成功的延续更加微妙。我们打印获得的值,然后使用retry函数重新调用内部循环,该函数将能够尝试下一个替代方案。这个next_alternative函数是传递给成功延续的第二个参数。通常,我们认为这第二个参数是一个失败延续,如果当前的求值分支后来失败了,就会使用它。然而,在这种情况下,我们已经完成了一个成功的求值,因此我们可以调用“失败”替代分支,以搜索额外的成功求值。

const input_prompt = "amb-求值输入:";
const output_prompt = "amb-求值值:";

function driver_loop(env) {
    function internal_loop(retry) {
        const input = user_read(input_prompt);
        if (is_null(input)) {
            display("evaluator terminated");
        } else if (input === "retry") {
            return retry();
        } else {
            display("Starting a new problem");
            const program = parse(input);
            const locals = scan_out_declarations(program);
            const unassigneds = list_of_unassigned(locals);
            const program_env = extend_environment(
                                     locals, unassigneds, env);
            return ambeval(
                       program,
                       program_env,
                       // ambeval success
                       (val, next_alternative) => {
                           user_print(output_prompt, val);
                           return internal_loop(next_alternative);
                       },
                       // ambeval failure
                       () => {
                           display("There are no more values of");
                           display(input);
                           return driver_loop(program_env);
                       });
        }
    }
    return internal_loop(() => {
                             display("There is no current problem");
                             return driver_loop(env);
                         });
}

internal_loop的初始调用使用retry函数,该函数抱怨当前没有问题并重新启动驱动循环。如果用户在没有进行求值时输入retry,则会发生这种行为。

我们像往常一样启动驱动循环,通过设置全局环境并将其作为第一次迭代的封闭环境传递给driver_loop

const the_global_environment = setup_environment();
driver_loop(the_global_environment);
练习 4.48

实现一个新的语法形式ramb,它类似于amb,但是以随机顺序搜索替代,而不是从左到右。展示这如何帮助艾丽莎在练习 4.47 中的问题。

练习 4.49

更改赋值的实现,使其在失败时不会被撤消。例如,我们可以从列表中选择两个不同的元素,并计算成功选择所需的尝试次数如下:

let count = 0;

let x = an_element_of("a", "b", "c");
let y = an_element_of("a", "b", "c"); count = count + 1;
require(x !== y); list(x, y, count);

开始新问题:

amb-求值值:

["a", ["b", [2, null]]]

amb-求值输入:

retry

amb-求值值:

["a", ["c", [3, null]]]

如果我们使用了赋值的原始含义而不是永久赋值,将显示哪些值?

练习 4.50

我们将可怕地滥用条件语句的语法,通过实现以下形式的结构:

if (evaluation_succeeds_take) { statement } else { alternative }

该结构允许用户捕获语句的失败。它像往常一样求值语句,如果求值成功,则像往常一样返回。但是,如果求值失败,将求值给定的替代语句,如下例所示:

amb-求值输入:

if (evaluation_succeeds_take) {
    const x = an_element_of(list(1, 3, 5));
    require(is_even(x));
    x;
} else {
    "all odd";
}

开始一个新问题

amb-求值值:

"all odd"

amb-求值输入:

if (evaluation_succeeds_take) {
    const x = an_element_of(list(1, 3, 5, 8));
    require(is_even(x));
    x;
} else {
    "all odd";
}

开始一个新问题

amb-求值值:

`8`

通过扩展amb求值器来实现此结构。提示:函数is_amb显示了如何滥用现有的 JavaScript 语法以实现新的语法形式。

练习 4.51

使用练习 4.49 中描述的新类型的赋值和结构

if (evaluation_succeeds_take) { ... } else { ... }

就像在练习 4.50 中一样,求值的结果将是什么

let pairs = null;
if (evaluation_succeeds_take) {
    const p = prime_sum_pair(list(1, 3, 5, 8), list(20, 35, 110));
    pairs = pair(p, pairs); // using permanent assignment
    amb();
} else {
    pairs;
}
练习 4.52

如果我们没有意识到require可以作为一个普通函数来实现,该函数使用amb,由用户作为非确定性程序的一部分来定义,我们将不得不将其实现为一个语法形式。这将需要语法函数

function is_require(component) {
    return is_tagged_list(component, "require");
}
function require_predicate(component) { return head(tail(component)); }

以及在analyze的调度中的新子句

: is_require(component)
? analyze_require(component)

以及处理require表达式的analyze_require函数。完成analyze_require的以下定义。

function analyze_require(component) {
    const pfun = analyze(require_predicate(component));
    return (env, succeed, fail) =>
            pfun(env,
                 (pred_value, fail2) =>
                   〈??〉
                   ? 〈??〉
                   : succeed("ok", fail2),
                 fail);
}

4.4 逻辑编程

在第 1 章中,我们强调计算机科学涉及命令式(如何)知识,而数学涉及声明式(什么是)知识。事实上,编程语言要求程序员以一种形式表达知识,该形式指示解决特定问题的逐步方法。另一方面,高级语言作为语言实现的一部分,提供了大量的方法论知识,使用户不必关心特定计算的进展细节。

大多数编程语言,包括 JavaScript,都是围绕计算数学函数的值而组织的。面向表达式的语言(如 Lisp、C、Python 和 JavaScript)利用了一个“双关语”,即描述函数值的表达式也可以被解释为计算该值的手段。因此,大多数编程语言都倾向于单向计算(具有明确定义的输入和输出的计算)。然而,也有根本不同的编程语言放松了这种偏见。我们在 3.3.5 节中看到了一个这样的例子,其中计算的对象是算术约束。在约束系统中,计算的方向和顺序没有那么明确定义;因此,在进行计算时,系统必须提供比普通算术计算更详细的“如何”知识。然而,这并不意味着用户完全摆脱了提供命令性知识的责任。有许多约束网络实现了相同的约束集,用户必须从数学上等价的网络集合中选择一个合适的网络来指定特定的计算。

第 4.3 节的非确定性程序求值器也摆脱了编程是关于构建计算单向函数的观点。在非确定性语言中,表达式可以有多个值,因此计算处理的是关系而不是单值函数。逻辑编程通过将编程的关系视野与一种称为统一的强大的符号模式匹配相结合来扩展这一思想。

这种方法在起作用时可以是编写程序的一种非常强大的方式。部分原因在于一个“是什么”事实可以用来解决许多不同的问题,这些问题可能有不同的“如何”组成部分。例如,考虑append操作,它接受两个列表作为参数,并将它们的元素组合成一个单一的列表。在 JavaScript 等过程式语言中,我们可以根据基本的列表构造函数pair来定义append,就像我们在 2.2.1 节中所做的那样。

function append(x, y) {
    return is_null(x)
           ? y
           : pair(head(x), append(tail(x), y));
}

这个函数可以被看作是将以下两条规则翻译成 JavaScript,第一条规则涵盖了第一个列表为空的情况,第二条规则处理了非空列表的情况,即两个部分的pair

  • 对于任何列表y,空列表和y append形成y

  • 对于任何uvyz,如果pair(u, v)y append形成pair(u, z),那么vy append形成z

使用append函数,我们可以回答诸如

找到list("a", "b")list("c", "d")append

但是,同样的两条规则也足以回答以下类型的问题,而函数无法回答:

找到一个列表y,它与list("a", "b")一起append以产生

    list("a", "b", "c", "d").

找到所有xy,它们append形成list("a", "b", "c", "d")

在逻辑编程语言中,程序员通过陈述上述关于append的两条规则来编写append“函数”。解释器会自动提供“如何”知识,以便使用这一对规则来回答关于append的所有三种类型的问题。

当代逻辑编程语言(包括我们在这里实现的语言)存在重大缺陷,因为它们的一般“如何”方法可能会导致它们陷入虚假的无限循环或其他不良行为。逻辑编程是计算机科学中的一个活跃研究领域。

在本章的前面,我们探讨了实现解释器的技术,并描述了对于类似 JavaScript 的语言(实际上,对于任何传统语言)的解释器所必不可少的元素。现在我们将应用这些想法来讨论逻辑编程语言的解释器。我们将这种语言称为查询语言,因为它非常适用于通过用语言表达的查询或问题来从数据库中检索信息。尽管查询语言与 JavaScript 非常不同,但我们将发现用相同的一般框架来描述语言是方便的:作为原始元素的集合,以及使我们能够将简单元素组合成更复杂元素的组合手段和使我们能够将复杂元素视为单个概念单位的抽象手段。逻辑编程语言的解释器比像 JavaScript 这样的语言的解释器复杂得多。尽管如此,我们将看到我们的查询语言解释器包含了在第 4.1 节的解释器中找到的许多相同元素。特别是,将有一个“求值”部分,根据类型对表达式进行分类,以及一个“应用”部分,实现语言的抽象机制(JavaScript 的情况下是函数,逻辑编程的情况下是规则)。此外,实现中的一个核心作用是由框架数据结构发挥的,它确定了符号和它们关联值之间的对应关系。我们查询语言实现的另一个有趣方面是我们大量使用了流,这在第 3 章中介绍过。

4.4.1 推理信息检索

逻辑编程在提供接口以用于信息检索的数据库方面表现出色。我们将在本章实现的查询语言旨在以这种方式使用。

为了说明查询系统的功能,我们将展示如何使用它来管理波士顿地区蓬勃发展的高科技公司 Gargle 的人员记录数据库。该语言提供了对人员信息的模式导向访问,并且还可以利用一般规则进行逻辑推断。

一个样本数据库

Gargle 的人员数据库包含有关公司人员的断言。以下是有关 Ben Bitdiddle 的信息,他是公司的计算机专家:

address(list("Bitdiddle", "Ben"),
        list("Slumerville", list("Ridge", "Road"), 10))
job(list("Bitdiddle", "Ben"), list("computer", "wizard"))
salary(list("Bitdiddle", "Ben"), 122000)

断言看起来就像 JavaScript 中的函数应用,但实际上它们代表了数据库中的信息。第一个符号——这里是addressjobsalary——描述了各自断言中包含的信息种类,而“参数”是列表或原始值,如字符串和数字。第一个符号不需要像 JavaScript 中的常量或变量那样被声明;它们的范围是全局的。

作为公司的专家,Ben 负责公司的计算机部门,并监督两名程序员和一名技术员。以下是关于他们的信息:

address(list("Hacker", "Alyssa", "P"),
        list("Cambridge", list("Mass", "Ave"), 78))
job(list("Hacker", "Alyssa", "P"), list("computer", "programmer"))
salary(list("Hacker", "Alyssa", "P"), 81000)
supervisor(list("Hacker", "Alyssa", "P"), list("Bitdiddle", "Ben"))

address(list("Fect", "Cy", "D"),
        list("Cambridge", list("Ames", "Street"), 3))
job(list("Fect", "Cy", "D"), list("computer", "programmer"))
salary(list("Fect", "Cy", "D"), 70000)
supervisor(list("Fect", "Cy", "D"), list("Bitdiddle", "Ben"))

address(list("Tweakit", "Lem", "E"),
        list("Boston", list("Bay", "State", "Road"), 22))
job(list("Tweakit", "Lem", "E"), list("computer", "technician"))
salary(list("Tweakit", "Lem", "E"), 51000)
supervisor(list("Tweakit", "Lem", "E"), list("Bitdiddle", "Ben"))

还有一名程序员实习生,由 Alyssa 监督:

address(list("Reasoner", "Louis"),
        list("Slumerville", list("Pine", "Tree", "Road"), 80))
job(list("Reasoner", "Louis"),
        list("computer", "programmer", "trainee"))
salary(list("Reasoner", "Louis"), 62000)
supervisor(list("Reasoner", "Louis"), list("Hacker", "Alyssa", "P"))

所有这些人都在计算机部门,这可以从他们的工作描述中的第一个项目为“计算机”这个词来看出。

Ben 是一名高级雇员。他的主管是公司的大佬本人:

supervisor(list("Bitdiddle", "Ben"), list("Warbucks", "Oliver"))

address(list("Warbucks", "Oliver"),
        list("Swellesley", list("Top", "Heap", "Road")))
job(list("Warbucks", "Oliver"), list("administration", "big", "wheel"))
salary(list("Warbucks", "Oliver"), 314159)

除了由 Ben 监督的计算机部门,公司还有一个会计部门,由一名总会计和他的助手组成:

address(list("Scrooge", "Eben"),
        list("Weston", list("Shady", "Lane"), 10))
job(list("Scrooge", "Eben"), list("accounting", "chief", "accountant"))
salary(list("Scrooge", "Eben"), 141421)
supervisor(list("Scrooge", "Eben"), list("Warbucks", "Oliver"))

address(list("Cratchit", "Robert"),
        list("Allston", list("N", "Harvard", "Street"), 16))
job(list("Cratchit", "Robert"), list("accounting", "scrivener"))
salary(list("Cratchit", "Robert"), 26100)
supervisor(list("Cratchit", "Robert"), list("Scrooge", "Eben"))

公司的大佬还有一名行政助理:

address(list("Aull", "DeWitt"),
        list("Slumerville", list("Onion", "Square"), 5))
job(list("Aull", "DeWitt"), list("administration", "assistant"))
salary(list("Aull", "DeWitt"), 42195)
supervisor(list("Aull", "DeWitt"), list("Warbucks", "Oliver"))

数据库还包含有关持有其他种类工作的人可以做哪种工作的断言。例如,计算机专家可以做计算机程序员和计算机技术员的工作:

can_do_job(list("computer", "wizard"),
           list("computer", "programmer"))
can_do_job(list("computer", "wizard"),
           list("computer", "technician"))

计算机程序员可以代替实习生:

can_do_job(list("computer", "programmer"),
           list("computer", "programmer", "trainee"))

此外,众所周知,

can_do_job(list("administration", "assistant"),
           list("administration", "big", "wheel"))
简单查询

查询语言允许用户通过对系统提示的查询来从数据库中检索信息。例如,要找到所有计算机程序员,可以说

查询输入:

job($x, list("computer", "programmer"))

系统将响应以下项目:

查询结果:

job(list("Hacker", "Alyssa", "P"), list("computer", "programmer"))

job(list("Fect", "Cy", "D"), list("computer", "programmer"))

输入查询指定我们正在寻找与数据中的某个模式匹配的条目。在这个例子中,模式指定job作为我们正在寻找的信息类型。第一项可以是任何东西,第二项是字面上的列表list("computer", "programmer")。匹配断言中可以作为第一项的“任何东西”由模式变量$x指定。作为模式变量,我们使用以美元符号开头的 JavaScript 名称。我们将在下面看到为什么指定模式变量的名称比只在模式中放入一个符号(比如$)来代表“任何东西”更有用。系统通过显示所有与指定模式匹配的数据中的条目来响应简单查询。

模式可以有多个变量。例如,查询

address($x, $y)

将列出所有员工的地址。

模式可以没有变量,这种情况下查询只是确定该模式是否是数据中的一个条目。如果是,将会有一个匹配;如果不是,将没有匹配。

相同的模式变量可以在查询中出现多次,指定相同的“任何东西”必须出现在每个位置。这就是为什么变量有名称。例如,

supervisor($x, $x)

查找所有监督自己的人(尽管在我们的样本数据库中没有这样的断言)。

查询

job($x, list("computer", $type))

匹配所有工作条目,其第二项是一个第一项为"computer"的两元素列表:

job(list("Bitdiddle", "Ben"), list("computer", "wizard"))
job(list("Hacker", "Alyssa", "P"), list("computer", "programmer"))
job(list("Fect", "Cy", "D"), list("computer", "programmer"))
job(list("Tweakit", "Lem", "E"), list("computer", "technician"))

这个模式匹配

job(list("Reasoner", "Louis"),
    list("computer", "programmer", "trainee"))

因为断言中的第二项是一个三元素列表,而模式的第二项指定应该有两个元素。如果我们想要更改模式,使得第二项可以是以"computer"开头的任何列表,我们可以指定

job($x, pair("computer", $type))

例如,

pair("computer", $type)

匹配数据

list("computer", "programmer", "trainee")

$typelist("programmer", "trainee")。它也匹配数据

list("computer", "programmer")

$typelist("programmer"),并匹配数据

list("computer")

$type为空列表,null

我们可以描述查询语言对简单查询的处理如下:

  • 系统找到查询模式中变量的所有赋值,这些赋值满足该模式——也就是说,所有变量的值集合,使得如果模式变量被实例化为(替换为)这些值,结果就在数据中。

  • 系统通过列出满足查询模式的变量赋值来响应查询。

注意,如果模式没有变量,查询将简化为确定该模式是否在数据中。如果是,空赋值将满足该模式。

练习 4.53

给出从数据库中检索以下信息的简单查询:

  1. a. 由 Ben Bitdiddle 监督的所有人;

  2. b. 会计部门所有人的姓名和工作;

  3. c. 居住在 Slumerville 的所有人的姓名和地址。

复合查询

简单查询形成了查询语言的原始操作。为了形成复合操作,查询语言提供了组合的手段。查询语言成为逻辑编程语言的一个原因是,组合的手段反映了形成逻辑表达式时使用的组合手段:andornot

我们可以使用and如下来找到所有计算机程序员的地址:

and(job($person, list("computer", "programmer")),
    address($person, $where))

结果输出为

and(job(list("Hacker", "Alyssa", "P"), list("computer", "programmer")),
    address(list("Hacker", "Alyssa", "P"),
            list("Cambridge", list("Mass", "Ave"), 78)))
and(job(list("Fect", "Cy", "D"), list("computer", "programmer")),
    address(list("Fect", "Cy", "D"),
            list("Cambridge", list("Ames", "Street"), 3)))

一般来说,

and(query[1], query[2], ..., query[n])

由同时满足query[1],...,query[n]的模式变量的值集合来满足模式。

对于简单查询,系统通过找到满足查询的模式变量的所有赋值,然后显示具有这些值的查询的实例化来处理复合查询。

构建复合查询的另一种方法是通过or。例如,

or(supervisor($x, list("Bitdiddle", "Ben")),
   supervisor($x, list("Hacker", "Alyssa", "P")))

将找到所有由 Ben Bitdiddle 或 Alyssa P. Hacker 监督的员工:

or(supervisor(list("Hacker", "Alyssa", "P"),
              list("Bitdiddle", "Ben")),
   supervisor(list("Hacker", "Alyssa", "P"),
              list("Hacker", "Alyssa", "P")))

or(supervisor(list("Fect", "Cy", "D"),
              list("Bitdiddle", "Ben")),
   supervisor(list("Fect", "Cy", "D"),
              list("Hacker", "Alyssa", "P")))

or(supervisor(list("Tweakit", "Lem", "E"),
              list("Bitdiddle", "Ben")),
   supervisor(list("Tweakit", "Lem", "E"),
              list("Hacker", "Alyssa", "P")))

or(supervisor(list("Reasoner", "Louis"),
              list("Bitdiddle", "Ben")),
   supervisor(list("Reasoner", "Louis"),
              list("Hacker", "Alyssa", "P")))

一般来说,

or(query[1], query[2], ..., query[n])

由满足至少一个query[1] ... query[n]的模式变量的值集合满足。

复合查询也可以用not形成。例如,

and(supervisor($x, list("Bitdiddle", "Ben")),
    not(job($x, list("computer", "programmer"))))

找到所有由 Ben Bitdiddle 监督的不是计算机程序员的人。一般来说,

not(query[1])

由不满足query[1]的模式变量的所有赋值满足。⁵⁷

最终的组合形式以javascript_predicate开头,包含一个 JavaScript 谓词。一般来说,

javascript_predicate(predicate)

将满足predicate中实例化的predicate为真的模式变量的赋值。例如,要找到所有工资大于 5 万美元的人,我们可以写⁵⁸

and(salary($person, $amount), javascript_predicate($amount > 50000))
练习 4.54

制定检索以下信息的复合查询:

  1. a. 所有由 Ben Bitdiddle 监督的人的姓名,以及他们的地址;

  2. b. 所有工资低于 Ben Bitdiddle 的人,以及他们的工资和 Ben Bitdiddle 的工资;

  3. c. 所有由不在计算机部门的人监督的人,以及主管的姓名和工作。

规则

除了原始查询和复合查询,查询语言还提供了抽象查询的手段。这些由规则给出。规则

rule(lives_near($person_1, $person_2),
    and(address($person_1, pair($town, $rest_1)),
        address($person_2, pair($town, $rest_2)),
        not(same($person_1, $person_2))))

指定如果两个人住在同一个城镇,那么他们住在附近。最后的not子句防止规则说所有人都住在自己附近。same关系由一个非常简单的规则定义:⁵⁹

rule(same($x, $x))

以下规则声明,如果一个人监督一个人,而这个人又是一个主管,那么这个人在组织中是一个“轮子”:

rule(wheel($person),
     and(supervisor($middle_manager, $person),
         supervisor($x, $middle_manager)))

规则的一般形式是

rule(conclusion, body)

其中conclusion是一个模式,body是任何查询。⁶⁰我们可以认为规则代表一个大(甚至无限)的断言集,即满足规则体的变量赋值的规则结论的所有实例化。当我们描述简单查询(模式)时,我们说变量的赋值满足模式,如果实例化的模式在数据库中。但是模式不一定要作为断言明确地在数据库中。它可以是由规则暗示的隐式断言。例如,查询

lives_near($x, list("Bitdiddle", "Ben"))

导致

lives_near(list("Reasoner", "Louis"), list("Bitdiddle", "Ben"))

lives_near(list("Aull", "DeWitt"), list("Bitdiddle", "Ben"))

要找到所有住在 Ben Bitdiddle 附近的计算机程序员,我们可以问

and(job($x, list("computer", "programmer")),
    lives_near($x, list("Bitdiddle", "Ben")))

与复合函数一样,规则可以作为其他规则的一部分(就像我们上面看到的lives_near规则),甚至可以递归地定义。例如,规则

rule(outranked_by($staff_person, $boss),
     or(supervisor($staff_person, $boss),
        and(supervisor($staff_person, $middle_manager),
            outranked_by($middle_manager, $boss))))

说一个员工在组织中被老板超越,如果老板是这个人的主管,或者(递归地)如果这个人的主管被老板超越。

练习 4.55

定义一个规则,规定如果人 1 和人 2 做同样的工作,或者做人 1 的工作的人也可以做人 2 的工作,那么人 1 可以取代人 2,前提是人 1 和人 2 不是同一个人。使用你的规则,给出以下查询:

  1. a. 所有可以取代 Cy D. Fect 的人;

  2. b. 所有可以取代收入比他们高的人的人,以及两个人的工资。

练习 4.56

定义一个规则,如果一个人在部门中工作但没有一个在部门中工作的主管,那么这个人在部门中是一个“大人物”。

练习 4.57

Ben Bitdiddle 错过了太多次会议。担心他忘记会议的习惯可能会让他失去工作,Ben 决定采取一些行动。他通过断言将公司的所有周会议添加到 Gargle 数据库中,如下所示:

meeting("accounting", list("Monday", "9am"))
meeting("administration", list("Monday", "10am"))
meeting("computer", list("Wednesday", "3pm"))
meeting("administration", list("Friday", "1pm"))

上述每个断言都是整个部门的会议。Ben 还为跨越所有部门的全公司会议添加了一个条目。公司的所有员工都参加这次会议。

meeting("whole-company", list("Wednesday", "4pm"))
  1. a. 在星期五早上,Ben 想要查询当天发生的所有会议。他应该使用什么查询?

  2. b. Alyssa P. Hacker 并不感到满意。她认为,通过指定她的名字来询问她的会议将会更有用。因此,她设计了一个规则,规定一个人的会议包括所有“整公司”会议以及该人所在部门的所有会议。填写 Alyssa 的规则的主体。

    rule(meeting_time($person, $day_and_time),
         rule-body)
    
  3. c. Alyssa 在周三早上到达工作岗位,想知道当天她需要参加哪些会议。在定义了上述规则之后,她应该提出什么查询来找出这一点?

练习 4.58

通过给出查询

lives_near($person, list("Hacker", "Alyssa", "P"))

Alyssa P. Hacker 能够找到住在她附近的人,可以和他们一起上班。另一方面,当她尝试通过查询找到所有住在附近的人的对时

lives_near($person_1, $person_2)

她注意到每对住在附近的人都被列出了两次;例如,

lives_near(list("Hacker", "Alyssa", "P"), list("Fect", "Cy", "D"))
lives_near(list("Fect", "Cy", "D"), list("Hacker", "Alyssa", "P"))

为什么会发生这种情况?有没有办法找到住在附近的人的名单,其中每对只出现一次?请解释。

逻辑作为程序

我们可以将规则视为一种逻辑蕴涵:如果对模式变量的值的分配满足主体,那么它满足结论。因此,我们可以认为查询语言具有根据规则执行逻辑推导的能力。例如,考虑第 4.4 节开头描述的append操作。正如我们所说的,append可以由以下两个规则来描述:

  • 对于任何列表y,空列表和y appendy

  • 对于任何uvyz,如果vy appendz,则pair(u, v)y appendpair(u, z)

为了在我们的查询语言中表达这一点,我们为一个关系定义了两个规则

append_to_form(x, y, z)

我们可以将其解释为“xy appendz”:

rule(append_to_form(null, $y, $y))

rule(append_to_form(pair($u, $v), $y, pair($u, $z)),
     append_to_form($v, $y, $z))

第一个规则没有主体,这意味着结论对于任何$y的值都成立。请注意第二个规则如何利用pair来命名列表的头部和尾部。

在给定这两个规则的情况下,我们可以制定计算两个列表的append的查询:

查询输入:

append_to_form(list("a", "b"), list("c", "d"), $z)

查询结果:

append_to_form(list("a", "b"), list("c", "d"), list("a", "b", "c", "d"))

更令人惊讶的是,我们可以使用相同的规则来询问“哪个列表appendlist("a", "b")会产生list("a", "b", "c", "d")?” 这样做如下:

查询输入:

append_to_form(list("a", "b"), $y, list("a", "b", "c", "d"))

查询结果:

append_to_form(list("a", "b"), list("c", "d"), list("a", "b", "c", "d"))

我们可以要求所有appendlist("a", "b", "c", "d")的列表对:

查询输入:

append_to_form($x, $y, list("a", "b", "c", "d"))

查询结果:

append_to_form(null, list("a", "b", "c", "d"), list("a", "b", "c", "d"))

append_to_form(list("a"), list("b", "c", "d"), list("a", "b", "c", "d"))

append_to_form(list("a", "b"), list("c", "d"), list("a", "b", "c", "d"))

append_to_form(list("a", "b", "c"), list("d"), list("a", "b", "c", "d"))

append_to_form(list("a", "b", "c", "d"), null, list("a", "b", "c", "d"))

查询系统似乎在使用规则推断上述查询的答案时表现出相当多的智能。实际上,正如我们将在下一节中看到的那样,系统正在遵循一个明确定义的算法来解开规则。不幸的是,尽管系统在append情况下的工作令人印象深刻,但一般方法可能会在更复杂的情况下崩溃,正如我们将在第 4.4.3 节中看到的那样。

练习 4.59

以下规则实现了一个next_to_in关系,找到列表的相邻元素:

rule(next_to_in($x, $y, pair($x, pair($y, $u))))

rule(next_to_in($x, $y, pair($v, $z)),
     next_to_in($x, $y, $z))

对以下查询的响应将是什么?

next_to_in($x, $y, list(1, list(2, 3), 4))
next_to_in($x, 1, list(2, 1, 3, 1))
练习 4.60

定义规则来实现练习 2.17 的last_pair操作,该操作返回包含非空列表的最后一个元素的列表。在以下查询中检查您的规则:

  • last_pair(list(3), $x)

  • last_pair(list(1, 2, 3), $x)

  • last_pair(list(2, $x), list(3))

您的规则在诸如last_pair($x, list(3))的查询上是否正确工作?

练习 4.61

以下数据库(参见创世记 4)追溯了亚当的后裔的家谱,经由该隐:

son("Adam", "Cain")
son("Cain", "Enoch")
son("Enoch", "Irad")
son("Irad", "Mehujael") son("Mehujael", "Methushael") son("Methushael", "Lamech") wife("Lamech", "Ada")
son("Ada", "Jabal")
son("Ada", "Jubal")

制定规则,例如“如果SF的儿子,FG的儿子,那么SG的孙子”,以及“如果WM的妻子,SW的儿子,那么SM的儿子”(这在圣经时代比今天更真实),这将使查询系统能够找到该隐的孙子;拉麦的儿子;麦土撒的孙子。(参见练习 4.67,了解推断更复杂关系的一些规则。)

4.4.2 查询系统的工作原理

在第 4.4.4 节中,我们将介绍查询解释器的一组函数实现。在本节中,我们将概述解释器的一般结构,独立于低级实现细节。在描述解释器的实现之后,我们将能够理解查询语言的逻辑操作与数学逻辑操作的一些微妙差异以及一些限制。

显然,查询求值器必须执行某种搜索,以便将查询与数据库中的事实和规则匹配。一种方法是将查询系统实现为一个非确定性程序,使用第 4.3 节的amb求值器(参见练习 4.75)。另一种可能性是使用流来管理搜索。我们的实现遵循第二种方法。

查询系统围绕两个中心操作组织,称为模式匹配统一。我们首先描述模式匹配,并解释这个操作,以及信息的组织方式,以流的形式的帧,使我们能够实现简单和复合查询。接下来我们讨论统一,这是模式匹配的一般化,需要实现规则。最后,我们展示整个查询解释器如何通过一个函数组合在一起,类似于第 4.1 节中描述的解释器对表达式进行分类的方式。

模式匹配

模式匹配器是一个测试某个数据是否符合指定模式的程序。例如,数据list(list("a", "b"), "c", list("a", "b"))与模式list($x, "c", $x)匹配,其中模式变量$x绑定到list("a", "b")。相同的数据列表与模式list($x, $y, $z)匹配,其中$x$z都绑定到list("a", "b")$y绑定到"c"。它还与模式list(list($x, $y), "c", list($x, $y))匹配,其中$x绑定到"a"$y绑定到"b"。但是,它不与模式list($x, "a", $y)匹配,因为该模式指定了第二个元素为字符串"a"的列表。

查询系统使用的模式匹配器将模式、数据和指定各种模式变量绑定的作为输入。它检查数据是否与模式匹配,并且与帧中已有的绑定一致。如果是,它返回给定的帧,其中可能包含由匹配确定的任何绑定。否则,它指示匹配失败。

使用模式list($x, $y, $x)来匹配list("a", "b", "a"),给定一个空帧,例如,将返回一个指定$x绑定为"a"$y绑定为"b"的帧。尝试使用相同的模式、相同的数据和指定$y绑定为"a"的帧进行匹配将失败。尝试使用相同的模式、相同的数据和一个帧,其中$y绑定为"b"$x未绑定,将返回给定的帧,增加了$x绑定为"a"

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

job($x, list("computer", "programmer"))

我们扫描数据库中的所有断言,并选择与最初空帧相匹配的断言。对于我们找到的每个匹配,我们使用匹配返回的帧来实例化具有$x值的模式。

帧流

通过使用流,模式对帧的测试是有组织的。给定一个单个帧,匹配过程逐个运行数据库条目。对于每个数据库条目,匹配器生成一个特殊符号,指示匹配失败,或者是帧的扩展。所有数据库条目的结果被收集到一个流中,通过过滤器传递以清除失败。结果是所有通过与数据库中某个断言的匹配扩展给定帧的所有帧的流。⁶¹

在我们的系统中,查询接受帧的输入流,并对流中的每个帧执行上述匹配操作,如图 4.5 所示。也就是说,对于输入流中的每个帧,查询生成一个新的流,其中包含通过与数据库中的断言匹配的该帧的所有扩展。然后将所有这些流组合成一个巨大的流,其中包含输入流中每个帧的所有可能扩展。这个流是查询的输出。

c4-fig-0005.jpg

图 4.5 查询处理帧流。

为了回答一个简单的查询,我们使用一个输入流,其中包含一个单个的空帧的查询。生成的输出流包含对空帧的所有扩展(即,对我们查询的所有答案)。然后使用这些帧的流来生成原始查询模式的副本流,其中变量由每个帧中的值实例化,这是最终打印的流。

复合查询

当我们处理复合查询时,流式帧实现的真正优雅之处就显而易见了。复合查询的处理利用了我们的匹配器要求匹配与指定帧一致的能力。例如,处理两个查询的and,如

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

(非正式地,“找到所有能够做计算机程序员实习生工作的人”),我们首先找到所有与模式匹配的条目

can_do_job($x, list("computer", "programmer", "trainee"))

这产生了一系列帧,每个帧都包含$x的绑定。然后对于流中的每个帧,我们找到所有与之匹配的条目

job($person, $x)

以与$x的给定绑定一致的方式。每个这样的匹配都将产生一个包含$x$person绑定的帧。两个查询的and可以被视为两个组成查询的串联组合,如图 4.6 所示。通过第一个查询过滤的帧将通过第二个查询进行进一步的过滤和扩展。

c4-fig-0006.jpg

图 4.6 两个查询的and组合是通过对帧流进行串联操作而产生的。

图 4.7 显示了计算两个查询的or的类似方法,作为两个组成查询的并联组合。输入的帧流分别由每个查询单独扩展。然后合并两个结果流以产生最终的输出流。

c4-fig-0007.jpg

图 4.7 两个查询的or组合是通过并行操作帧流并合并结果来产生的。

即使从这个高层描述中,处理复合查询的过程可能会很慢。例如,由于查询可能会为每个输入帧产生多个输出帧,并且and中的每个查询都从前一个查询中获取其输入帧,因此and查询在最坏的情况下可能需要执行指数数量的匹配(参见练习 4.73)。⁶² 尽管处理简单查询的系统非常实用,但处理复杂查询非常困难。⁶³

从帧流的观点来看,某些查询的not作为一个过滤器,删除所有可以满足查询的帧。例如,给定模式

not(job($x, list("computer", "programmer")))

我们尝试,对于输入流中的每个帧,产生满足job($x, list("computer", "programmer"))的扩展帧。我们从输入流中删除所有存在这样的扩展的帧。结果是一个仅由那些绑定$x不满足job($x, list("computer", "programmer"))的帧组成的流。例如,在处理查询时

and(supervisor($x, $y),
    not(job($x, list("computer", "programmer"))))

第一个子句将生成具有$x$y绑定的帧。然后,not子句将通过删除所有绑定$x满足$x是计算机程序员的限制的帧来过滤这些帧。⁶⁴

javascript_predicate语法形式被实现为帧流上的类似过滤器。我们使用流中的每个帧来实例化模式中的任何变量,然后应用 JavaScript 谓词。我们从输入流中删除所有谓词失败的帧。

统一

为了处理查询语言中的规则,我们必须能够找到结论与给定查询模式匹配的规则。规则结论类似于断言,只是它们可以包含变量,因此我们需要一种称为统一的模式匹配的泛化,其中“模式”和“数据”都可以包含变量。

统一器接受两个包含常量和变量的模式,并确定是否可能为变量分配值,使得两个模式相等。如果可以,它返回一个包含这些绑定的帧。例如,统一list($x, "a", $y)list($y, $z, "a")将指定一个帧,其中$x$y$z都必须绑定到"a"。另一方面,统一list($x, $y, "a")list($x, "b", $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"。我们可以将这个过程看作是在模式组件之间解方程组。一般来说,这些是同时方程,可能需要大量的操作来解决。⁶⁵ 例如,统一list($x, $x)list(list("a", $y, "c"), list("a", "b", $z))可以被认为是指定同时方程

$x = list("a", $y, "c")

$x = list("a", "b", $z)

这些方程意味着

list("a", $y, "c") = list("a", "b", $z)

这反过来意味着

"a" = "a", $y = "b", "c" = $z

因此

$x = list("a", "b", "c")

在成功的模式匹配中,所有模式变量都被绑定,它们被绑定的值只包含常量。到目前为止,我们所见到的所有统一的例子也是如此。然而,一般来说,成功的统一可能并不完全确定变量的值;一些变量可能保持未绑定,而其他变量可能绑定到包含变量的值。

考虑list($x, "a")list(list("b", $y), $z)的统一。我们可以推断$x=list("b", $y)"a"=$z,但我们无法进一步解决$x$y。统一不会失败,因为通过为$x$y分配值,可以使这两个模式相等。由于这种匹配无论如何都不会限制$y可以取的值,所以不会将$y的绑定放入结果帧中。但是,这种匹配确实限制了$x的值。无论$y有什么值,$x必须是list("b", $y)。因此,将$x绑定到模式list("b", $y)放入帧中。如果以后确定了$y的值并将其添加到帧中(通过需要与此帧一致的模式匹配或统一),则先前绑定的$x将引用此值。

应用规则

统一是查询系统的组成部分,用于从规则中进行推理。要了解如何实现这一点,可以考虑处理涉及应用规则的查询,例如

lives_near($x, list("Hacker", "Alyssa", "P"))

为了处理这个查询,我们首先使用上面描述的普通模式匹配函数来查看数据库中是否有任何与这个模式匹配的断言。(在这种情况下不会有任何匹配,因为我们的数据库中没有关于谁住在附近的直接断言。)下一步是尝试将查询模式与每条规则的结论统一。我们发现模式与规则的结论统一。

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))))

导致一个指定$x应该绑定到(具有与)$person_1相同的值,并且$person_2绑定到list("Hacker", "Alyssa", "P")的帧。现在,相对于这个帧,我们求值规则体给出的复合查询。成功的匹配将通过为$person_1提供绑定来扩展此帧,因此也会提供$x的值,我们可以用它来实例化原始查询模式。

一般来说,查询求值器在尝试在指定了一些模式变量绑定的帧中建立查询模式时,使用以下方法应用规则:

  • 将查询与规则的结论统一,如果成功,形成原始帧的扩展。

  • 相对于扩展帧,求值规则体形成的查询。

注意这与 JavaScript 中evaluate/apply求值器中应用函数的方法有多么相似:

  • 将函数的参数绑定到其参数以形成扩展原始函数环境的帧。

  • 相对于扩展环境,求值函数体形成的表达式。

这两个求值器之间的相似之处应该不足为奇。正如函数定义是 JavaScript 中的抽象手段一样,规则定义是查询语言中的抽象手段。在每种情况下,我们通过创建适当的绑定来解开抽象,并相对于这些绑定来求值规则或函数体。

简单查询

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

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

  • 通过将模式与数据库中的所有断言进行匹配获得的扩展帧的流(使用模式匹配器),

  • 通过应用所有可能的规则(使用统一器)获得的扩展帧的流。

将这两个流附加起来,产生的流包含了满足原始帧一致的给定模式的所有方式。这些流(每个输入流中的每个帧一个)现在都被组合成一个大流,因此包含了原始输入流中的任何帧扩展以产生与给定模式匹配的所有方式。

查询求值器和驱动循环

尽管底层匹配操作复杂,但系统的组织方式与任何语言的求值器类似。协调匹配操作的函数称为evaluate_query,它的作用类似于 JavaScript 的evaluate函数。函数evaluate_query的输入是一个查询和一个帧流。它的输出是一个帧流,对应于成功匹配查询模式的情况,这些帧扩展了输入流中的某个帧,如图 4.5 所示。与evaluate类似,evaluate_query对不同类型的表达式(查询)进行分类,并为每个类型的表达式调用适当的函数。对于每种句法形式(andornotjavascript_predicate)以及简单查询,都有一个函数。

驱动循环类似于本章其他求值器中的driver_loop函数,它读取用户键入的查询。对于每个查询,它调用evaluate_query,并提供查询和由单个空帧组成的流。这将产生所有可能匹配的流(所有可能的扩展到空帧的情况)。对于结果流中的每个帧,它使用帧中找到的变量的值来实例化原始查询。然后打印这些实例化的查询流。

驱动程序还检查特殊命令assert,该命令表示输入不是查询,而是要添加到数据库的断言或规则。例如,

assert(job(list("Bitdiddle", "Ben"), list("computer", "wizard")))

assert(rule(wheel($person),
            and(supervisor($middle_manager, $person),
                supervisor($x, $middle_manager))))

4.4.3 逻辑编程是数理逻辑吗?

查询语言中使用的组合方式乍看起来与数理逻辑中的“与”、“或”和“非”操作相同,事实上,查询语言规则的应用是通过一种合法的推理方法完成的。尽管如此,将查询语言与数理逻辑等同起来并不是真正有效的,因为查询语言提供了一个控制结构,以过程化方式解释逻辑语句。我们经常可以利用这种控制结构。例如,要找到所有程序员的主管,我们可以用两种逻辑上等价的形式来制定查询:

and(job($x, list("computer", "programmer")),
    supervisor($x, $y))

and(supervisor($x, $y),
    job($x, list("computer", "programmer")))

如果一个公司的主管比程序员多得多,最好使用第一种形式而不是第二种形式,因为数据库必须为第一个and子句产生的每个中间结果(帧)扫描。

逻辑编程的目的是为程序员提供将计算问题分解为两个独立问题的技术:“要计算什么”和“如何计算”。这是通过选择数理逻辑陈述的子集来实现的,该子集足够强大,可以描述任何想要计算的内容,但又足够弱,可以有可控的过程性解释。这里的意图是,一种逻辑编程语言中指定的程序应该是一个可以由计算机执行的有效程序。控制(“如何”计算)是通过使用语言的求值顺序来实现的。我们应该能够安排子句的顺序和每个子句内部子目标的顺序,以便以被认为是有效和高效的顺序进行计算。同时,我们应该能够将计算结果(“要计算什么”)视为逻辑法则的简单结果。

我们的查询语言可以被视为数学逻辑的一个过程可解释子集。一个断言代表一个简单的事实(一个原子命题)。规则代表规则结论成立的推论。规则有一个自然的过程解释:要建立规则的结论,要建立规则的主体。因此,规则指定了计算。然而,因为规则也可以被看作是数学逻辑的陈述,我们可以通过断言,即通过完全在数学逻辑中工作,来证明逻辑程序所完成的任何“推理”都是可以被证明的。⁷⁰

无限循环

逻辑程序的过程性解释的一个结果是,可以构建解决某些问题的无望低效程序。当系统陷入无限循环时,效率低下的极端情况发生。举个简单的例子,假设我们正在建立一个包括著名婚姻的数据库,包括

assert(married("Minnie", "Mickey"))

如果我们现在问

married("Mickey", $who)

我们将得不到任何回应,因为系统不知道如果AB结婚,那么B也与A结婚。因此,我们断言规则

assert(rule(married($x, $y),
            married($y, $x)))

再次查询

married("Mickey", $who)

不幸的是,这将使系统陷入无限循环,如下所示:

  • 系统发现married规则适用;也就是说,规则结论married($x, $y)与查询模式married("Mickey", $who)统一,产生一个帧,其中$x绑定为"Mickey"$y绑定为$who。因此,解释器继续在这个帧中求值规则主体married($y, $x),实际上是处理查询married($who, "Mickey")

  • 一个答案,married("Minnie", "Mickey"),直接出现在数据库中作为一个断言。

  • married规则也适用,因此解释器再次求值规则主体,这次等同于married("Mickey", $who)

系统现在陷入了无限循环。实际上,系统是否会在陷入循环之前找到简单的答案married("Minnie", "Mickey")取决于关于系统检查数据库中项目顺序的实现细节。这是可以发生的循环的一种非常简单的例子。相关规则的集合可能导致更难以预料的循环,并且循环的出现可能取决于and中子句的顺序(参见练习 4.62)或关于系统处理查询的顺序的低级细节。⁷¹

not的问题

查询系统中的另一个怪癖涉及not。给定第 4.4.1 节的数据库,考虑以下两个查询:

and(supervisor($x, $y),
    not(job($x, list("computer", "programmer"))))

and(not(job($x, list("computer", "programmer"))),
    supervisor($x, $y))

这两个查询不会产生相同的结果。第一个查询首先查找与supervisor($x, $y)匹配的数据库中的所有条目,然后通过删除满足job($x, list("computer", "programmer"))的值的结果帧来过滤结果。第二个查询首先通过过滤传入的帧来删除可以满足job($x, list("computer", "programmer"))的帧。由于唯一的传入帧是空的,它检查满足job($x, list("computer", "programmer"))的模式的数据库。由于通常存在这种形式的条目,not子句会过滤掉空帧并返回一个空的帧流。因此,整个复合查询返回一个空的帧流。

问题在于我们的not实现实际上是作为变量值的过滤器。如果在处理not子句时,一些变量保持未绑定(如上面的示例中的$x),系统将产生意外的结果。类似的问题也会出现在使用javascript_predicate时——如果其中一些变量未绑定,JavaScript 谓词将无法工作。参见练习 4.74。

查询语言的not与数学逻辑中的not有一种更为严重的不同之处。在逻辑中,我们解释语句“not P”表示P不是真的。然而,在查询系统中,“not P”表示P不能从数据库中的知识中推导出来。例如,给定第 4.4.1 节的人事数据,系统会愉快地推断出各种not语句,比如本·比迪德尔不是棒球迷,外面不下雨,2+2 不等于 4。换句话说,逻辑编程语言中的not反映了所谓的封闭世界假设,即所有相关信息都已包含在数据库中。

练习 4.62

路易斯·里森纳错误地从数据库中删除了outranked_by规则(第 4.4.1 节)。当他意识到这一点时,他迅速重新安装了它。不幸的是,他对规则进行了轻微更改,并将其输入为

rule(outranked_by($staff_person, $boss),
     or(supervisor($staff_person, $boss),
        and(outranked_by($middle_manager, $boss),
            supervisor($staff_person, $middle_manager))))

就在路易斯将这些信息输入系统后,德维特·奥尔过来询问谁的地位高于本·比迪德尔。他发出了查询

outanked_by(list("Bitdiddle", "Ben"), $who)

回答后,系统陷入无限循环。解释原因。

练习 4.63

赛伊·D·费克特期待着有一天能在组织中崛起,他提出了一个查询,以找到所有的车轮(使用第 4.4.1 节的wheel规则):

wheel($who)

令他惊讶的是,系统的回应是

查询结果:

wheel(list("Warbucks", "Oliver"))

wheel(list("Bitdiddle", "Ben"))

wheel(list("Warbucks", "Oliver"))

wheel(list("Warbucks", "Oliver"))

wheel(list("Warbucks", "Oliver"))

为什么奥利弗·沃巴克斯被列出了四次?

练习 4.64

本一直在将查询系统概括为提供有关公司的统计信息。例如,要找到所有计算机程序员的总薪水,可以说

sum($amount,
    and(job($x, list("computer", "programmer")),
        salary($x, $amount)))

一般来说,本的新系统允许形式的表达

accumulation_function(variable,
                      query-pattern)

其中accumulation_function可以是sumaveragemaximum之类的东西。本推断实现这应该很容易。他只需将查询模式提供给evaluate_query。这将产生一系列框架。然后,他将通过一个映射函数将这个流传递给累积函数,从而提取流中每个框架的指定变量的值,并将结果流传递给累积函数。就在本完成实现并准备尝试时,赛伊路过,仍在思考练习 4.63 中wheel查询结果。当赛伊向本展示系统的回应时,本叹息道:“哦,不,我的简单累积方案行不通!”

本刚刚意识到了什么?概述他可以用来挽救情况的方法。

练习 4.65

设计一种方法,在查询系统中安装一个循环检测器,以避免文本和练习 4.62 中所示的简单循环。一般的想法是,系统应该维护其当前推断链的某种历史,并且不应该开始处理它已经在处理的查询。描述这个历史中包含的信息(模式和框架),以及应该如何进行检查。(在你研究第 4.4.4 节中的查询系统实现的细节之后,你可能想修改系统以包括你的循环检测器。)

练习 4.66

定义规则来实现练习 2.18 的reverse操作,该操作以相反的顺序返回包含与给定列表相同元素的列表。(提示:使用append_to_form。)你的规则能回答查询reverse(list(1, 2, 3), $x)和查询reverse($x, list(1, 2, 3))吗?

练习 4.67

让我们修改练习 4.61 的数据库和规则,将great添加到孙子关系中。这应该使系统能够推断出伊拉德是亚当的曾孙,或者贾伯尔和尤巴尔是亚当的曾曾曾曾曾孙。

  1. a.更改数据库中的断言,使得只有一种关系信息,即related。第一项描述了关系。因此,不是son("Adam", "Cain"),而是related("son", "Adam", "Cain")。例如,表示关于 Irad 的事实为

    related(list("great", "grandson"), "Adam", "Irad")

  2. b.编写规则,确定列表是否以单词"grandson"结尾。

  3. c.使用这个来表达一个允许推导关系的规则

    list(pair("great", rel),rel), x, $y)

    其中$rel是以"grandson"结尾的列表。

  4. d.检查你的规则在查询related(list("great", "grandson"), $g, $ggs)related($relationship, "Adam", "Irad")上的表现。

4.4.4 实现查询系统

4.4.2 节描述了查询系统的工作原理。现在我们通过提供系统的完整实现来填写细节。

4.4.4.1 驱动循环

查询系统的驱动循环反复读取输入表达式。如果表达式是要添加到数据库中的规则或断言,那么信息就会被添加。否则,假定表达式是一个查询。驱动程序将此查询传递给evaluate_query,并与一个由单个空帧组成的初始帧流一起传递。求值的结果是通过满足在数据库中找到的变量值来生成的帧流。这些帧用于形成一个新的流,其中包含原始查询的副本,其中变量被帧流提供的值实例化,最终的流被显示:

const input_prompt = "Query input:";
const output_prompt = "Query results:";

function query_driver_loop() {
    const input = user_read(input_prompt) + ";";
    if (is_null(input)) {
        display("evaluator terminated");
    } else {
        const expression = parse(input);
        const query = convert_to_query_syntax(expression);
        if (is_assertion(query)) {
            add_rule_or_assertion(assertion_body(query));
            display("Assertion added to data base.");
        } else {
            display(output_prompt);
            display_stream(
              stream_map(
                 frame =>
                   unparse(instantiate_expression(expression, frame)),
                 evaluate_query(query, singleton_stream(null))));
        }
        return query_driver_loop();
    }
}

在这里,与本章中的其他求值器一样,我们使用parse将作为字符串给出的查询语言的组件转换为 JavaScript 语法表示。(我们在输入表达式字符串后附加了一个分号,因为parse期望一个语句。)然后我们进一步将语法表示转换为适合查询系统的概念级别,使用convert_to_query_syntax,它在 4.4.4.7 节中声明,以及谓词is_assertion和选择器assertion_body。函数add_rule_or_assertion在 4.4.4.5 节中声明。查询求值产生的帧用于实例化语法表示,结果被解析成字符串进行显示。函数instantiate_expressionunparse在 4.4.4.7 节中声明。

4.4.4.2 求值器

evaluate_query函数由query_driver_loop调用,是查询系统的基本求值器。它以查询和帧流作为输入,并返回扩展帧的流。它通过使用getput进行数据导向分派来识别句法形式,就像我们在第 2 章中实现通用操作一样。任何未被识别为句法形式的查询都被假定为简单查询,由simple_query处理。

function evaluate_query(query, frame_stream) {
    const qfun = get(type(query), "evaluate_query");
    return is_undefined(qfun)
           ? simple_query(query, frame_stream)
           : qfun(contents(query), frame_stream);
}

函数typecontents在 4.4.4.7 节中定义,实现了句法形式的抽象语法。

简单查询

simple_query函数处理简单查询。它以简单查询(模式)和帧流作为参数,并返回通过扩展每个帧的所有数据库匹配项形成的流。

function simple_query(query_pattern, frame_stream) {
    return stream_flatmap(
               frame =>
                 stream_append_delayed(
                     find_assertions(query_pattern, frame),
                     () => apply_rules(query_pattern, frame)),
               frame_stream);
}

对于输入流中的每个框架,我们使用find_assertions(第 4.4.4.3 节)来匹配数据库中所有断言与模式,产生一个扩展框架的流,并使用apply_rules(第 4.4.4.4 节)来应用所有可能的规则,产生另一个扩展框架的流。这两个流被合并(使用stream_append_delayed,第 4.4.4.6 节)以生成给定模式可以满足的所有方式的流,与原始框架一致(参见练习 4.68)。输入框架的流使用stream_flatmap(第 4.4.4.6 节)组合,形成一个大的流,列出原始输入流中任何框架可以扩展以与给定模式匹配的所有方式。

复合查询

我们处理and查询,如图 4.6 所示,使用conjoin函数,它以连接词和框架流作为输入,并返回扩展框架的流。首先,conjoin处理框架流以找到满足连接词中第一个查询的所有可能框架扩展的流。然后,使用这个新的框架流,它递归地将conjoin应用于其余的查询。

function conjoin(conjuncts, frame_stream) {
    return is_empty_conjunction(conjuncts)
           ? frame_stream
           : conjoin(rest_conjuncts(conjuncts),
                     evaluate_query(first_conjunct(conjuncts),
                                    frame_stream));
}

陈述

put("and", "evaluate_query", conjoin);

设置evaluate_query以在遇到and时分派到conjoin

我们类似地处理or查询,如图 4.7 所示。or的各个分离词的输出流分别计算,并使用第 4.4.4.6 节中的interleave_delayed函数合并。(参见练习 4.68 和 4.69。)

function disjoin(disjuncts, frame_stream) {
    return is_empty_disjunction(disjuncts)
           ? null
           : interleave_delayed(
                evaluate_query(first_disjunct(disjuncts), frame_stream),
                () => disjoin(rest_disjuncts(disjuncts), frame_stream));
}
put("or", "evaluate_query", disjoin);

在第 4.4.4.7 节中给出了表示连接词和分离词的谓词和选择器。

过滤器

not语法形式由第 4.4.2 节中概述的方法处理。我们尝试扩展输入流中的每个框架以满足被否定的查询,并且只有在不能扩展时才将给定框架包含在输出流中。

function negate(exps, frame_stream) {
    return stream_flatmap(
               frame =>
                 is_null(evaluate_query(negated_query(exps),
                                        singleton_stream(frame)))
                 ? singleton_stream(frame)
                 : null, frame_stream);
}
put("not", "evaluate_query", negate);

javascript_predicate语法形式类似于not的过滤器。流中的每个框架用于实例化谓词中的变量,实例化的谓词被求值,谓词求值为false的框架被过滤出输入流。使用evaluate(第 4.1 节)从the_global_environment求值实例化的谓词,因此可以处理任何 JavaScript 表达式,只要在求值之前实例化所有模式变量。

function javascript_predicate(exps, frame_stream) {
    return stream_flatmap(
               frame =>
                 evaluate(instantiate_expression(
                              javascript_predicate_expression(exps),
                              frame),
                          the_global_environment)
                 ? singleton_stream(frame)
                 : null,
               frame_stream);
}
put("javascript_predicate", "evaluate_query", javascript_predicate);

always_true语法形式提供了一个始终满足的查询。它忽略其内容(通常为空),并简单地通过输入流中的所有框架。rule_body选择器(第 4.4.4.7 节)使用always_true为没有定义体的规则提供体(即,始终满足的规则)。

function always_true(ignore, frame_stream) {
    return frame_stream;
}
put("always_true", "evaluate_query", always_true);

定义notjavascript_predicate的选择器在第 4.4.4.7 节中给出。

4.4.4.3 通过模式匹配查找断言

函数find_assertionssimple_query(第 4.4.4.2 节)调用,以模式和框架作为输入。它返回一个框架流,每个框架都通过给定模式的数据库匹配扩展给定框架。它使用fetch_assertions(第 4.4.4.5 节)获取数据库中所有断言的流,应该检查这些断言是否与模式和框架匹配。这里使用fetch_assertions的原因是我们通常可以应用简单的测试来消除数据库中的许多条目,使其不再是成功匹配的候选项。如果我们消除了fetch_assertions并简单地检查数据库中所有断言的流,系统仍然可以工作,但计算效率会降低,因为我们需要对匹配器进行更多的调用。

function find_assertions(pattern, frame) {
    return stream_flatmap(
                datum => check_an_assertion(datum, pattern, frame),
                fetch_assertions(pattern, frame));
}

函数check_an_assertion以数据对象(断言)、模式和框架作为参数,并返回一个包含扩展框架的单元素流,或者如果匹配失败则返回null

function check_an_assertion(assertion, query_pat, query_frame) {
    const match_result = pattern_match(query_pat, assertion,
                                       query_frame);
    return match_result === "failed"
           ? null
           : singleton_stream(match_result);
}

基本模式匹配器返回字符串failed或给定框架的扩展。匹配器的基本思想是逐个元素地检查模式与数据,累积模式变量的绑定。如果模式和数据对象相同,则匹配成功,我们返回迄今为止累积的绑定框架。否则,如果模式是一个变量(由 4.4.4.7 节中声明的is_variable函数检查),我们通过将变量绑定到数据来扩展当前框架,只要这与框架中已有的绑定一致。如果模式和数据都是对,我们(递归地)将模式的头与数据的头进行匹配以产生一个框架;然后在这个框架中,我们将模式的尾与数据的尾进行匹配。如果这些情况都不适用,则匹配失败,我们返回字符串failed

function pattern_match(pattern, data, frame) {
    return frame === "failed"
           ? "failed"
           : equal(pattern, data)
           ? frame
           : is_variable(pattern)
           ? extend_if_consistent(pattern, data, frame)
           : is_pair(pattern) && is_pair(data)
           ? pattern_match(tail(pattern),
                           tail(data),
                           pattern_match(head(pattern),
                                         head(data),
                                         frame))
           : "failed";
}

这是一个通过添加新绑定来扩展框架的函数,如果这与框架中已有的绑定一致的话:

function extend_if_consistent(variable, data, frame) {
    const binding = binding_in_frame(variable, frame);
    return is_undefined(binding)
           ? extend(variable, data, frame)
           : pattern_match(binding_value(binding), data, frame);
}

如果框架中没有变量的绑定,我们只需将变量的绑定添加到数据中。否则,我们在框架中将数据与框架中变量的值进行匹配。如果存储的值只包含常量,那么它必须在模式匹配期间由extend_if_consistent存储,匹配只是简单地测试存储的值和新值是否相同。如果是,则返回未修改的框架;如果不是,则返回失败指示。然而,存储的值可能包含模式变量,如果它是在统一期间存储的(见 4.4.4.4 节)。存储的模式与新数据的递归匹配将为这个模式中的变量添加或检查绑定。例如,假设我们有一个框架,其中$x绑定到list("f", $y),而$y未绑定,我们希望通过将$x绑定到list("f", "b")来扩充这个框架。我们查找$x并发现它绑定到list("f", $y)。这导致我们在同一个框架中将list("f", $y)与建议的新值list("f", "b")进行匹配。最终,这个匹配通过添加$y绑定到"b"来扩展框架。变量$x仍然绑定到list("f", $y)。我们从不修改存储的绑定,也不会为给定变量存储多个绑定。

extend_if_consistent使用的函数来操作绑定在 4.4.4.8 节中定义。

4.4.4.4 规则和统一

函数apply_rulesfind_assertions(4.4.4.3 节)的规则类比。它以模式和框架作为输入,并通过应用来自数据库的规则形成一个扩展框架流。函数stream_flatmapapply_a_rule映射到可能适用的规则流(由fetch_rules选择,4.4.4.5 节),并组合结果框架的流。

function apply_rules(pattern, frame) {
    return stream_flatmap(rule => apply_a_rule(rule, pattern, frame),
                          fetch_rules(pattern, frame));
}

函数apply_a_rule使用 4.4.2 节中概述的方法应用规则。它首先通过将规则结论与给定框架中的模式统一来扩充其参数框架。如果成功,它就在这个新框架中求值规则主体。

然而,在发生这些情况之前,程序会将规则中的所有变量重命名为唯一的新名称。这样做的原因是为了防止不同规则应用的变量相互混淆。例如,如果两个规则都使用名为$x的变量,那么每个规则在应用时可能都会向框架中添加一个$x的绑定。这两个$x互不相关,我们不应该被误导以为这两个绑定必须一致。我们可以设计一个更聪明的环境结构来代替重命名变量;然而,我们选择的重命名方法是最直接的,即使不是最有效的(见练习 4.76)。这里是apply_a_rule函数:

function apply_a_rule(rule, query_pattern, query_frame) {
    const clean_rule = rename_variables_in(rule);
    const unify_result = unify_match(query_pattern,
                                     conclusion(clean_rule),
                                     query_frame);
    return unify_result === "failed"
           ? null
           : evaluate_query(rule_body(clean_rule),
                            singleton_stream(unify_result));
}

提取规则的部分的选择器rule_bodyconclusion在 4.4.4.7 节中定义。

我们通过将唯一标识符(如数字)与每个规则应用关联,并将此标识符与原始变量名结合起来来生成唯一的变量名。例如,如果规则应用标识符为 7,我们可能会将规则中的每个$x更改为$x_7,将规则中的每个$y更改为$y_7。(函数make_new_variablenew_rule_application_id包含在第 4.4.4.7 节的语法函数中。)

function rename_variables_in(rule) {
    const rule_application_id = new_rule_application_id();
    function tree_walk(exp) {
        return is_variable(exp)
               ? make_new_variable(exp, rule_application_id)
               : is_pair(exp)
               ? pair(tree_walk(head(exp)),
                      tree_walk(tail(exp)))
               : exp;
    }
    return tree_walk(rule);
}

统一算法被实现为一个函数,它以两个模式和一个框架作为输入,并返回扩展的框架或字符串"failed"。统一器类似于模式匹配器,只是它是对称的 - 变量允许在匹配的两侧。函数unify_match基本上与pattern_match相同,只是下面有一个额外的子句(标记为***),用于处理右侧对象为变量的情况。

function unify_match(p1, p2, frame) {
    return frame === "failed"
           ? "failed"
           : equal(p1, p2)
           ? frame
           : is_variable(p1)
           ? extend_if_possible(p1, p2, frame)
           : is_variable(p2) // *
           ? extend_if_possible(p2, p1, frame) // *
           : is_pair(p1) && is_pair(p2)
           ? unify_match(tail(p1),
                         tail(p2),
                         unify_match(head(p1),
                                     head(p2),
                                     frame))
           : "failed";
}

在统一中,就像单向模式匹配一样,我们只希望接受框架的提议扩展,只有当它与现有绑定一致时才会这样。在统一中使用的函数extend_if_possible与模式匹配中使用的函数extend_if_consistent相同,只是在程序中有两个特殊检查,标记为***。在第一种情况下,如果我们尝试匹配的变量未绑定,但我们尝试匹配的值本身是(不同的)变量,则有必要检查该值是否已绑定,并且如果是,则匹配其值。如果匹配的双方都未绑定,我们可以将其中一个绑定到另一个。

第二个检查处理尝试将变量绑定到包含该变量的模式的情况。只要在两个模式中重复一个变量,这种情况就会发生。例如,考虑在两个模式list($x, $x)list($y, 包含 $y 的表达式)中统一,在其中$x$y都未绑定的框架中。首先,将$x$y匹配,将$x绑定到$y。接下来,将相同的$x与包含$y的给定表达式匹配。由于$x已绑定到$y,这导致将$y与表达式匹配。如果我们认为统一器是在找到一组使模式相同的模式变量的值,那么这些模式暗示了查找一个$y,使得$y等于包含$y的表达式。我们拒绝这样的绑定;这些情况由谓词depends_on识别。另一方面,我们不希望拒绝将变量绑定到自身的尝试。例如,考虑统一list($x, $x)list($y, $y)。将$x绑定到$y的第二次尝试将$y$x的存储值)与$y$x的新值)匹配。这由unify_matchequal子句处理。

function extend_if_possible(variable, value, frame) {
    const binding = binding_in_frame(variable, frame);
    if (! is_undefined(binding)) {
        return unify_match(binding_value(binding),
                            value, frame);
    } else if (is_variable(value)) { // *
         const binding = binding_in_frame(value, frame);
        return ! is_undefined(binding)
               ? unify_match(variable,
                             binding_value(binding),
                             frame)
               : extend(variable, value, frame);
    } else if (depends_on(value, variable, frame)) { // *
        return "failed";
    } else {
        return extend(variable, value, frame);
    }
}

函数depends_on是一个谓词,用于测试提议作为模式变量值的表达式是否依赖于该变量。这必须相对于当前框架来完成,因为表达式可能包含已经依赖于我们测试变量的值的变量的出现。depends_on的结构是一个简单的递归树遍历,我们在必要时替换变量的值。

function depends_on(expression, variable, frame) {
    function tree_walk(e) {
        if (is_variable(e)) {
            if (equal(variable, e)) {
                return true;
            } else {
                const b = binding_in_frame(e, frame);
                return is_undefined(b)
                       ? false
                       : tree_walk(binding_value(b));
            }
        } else {
            return is_pair(e)
                   ? tree_walk(head(e)) || tree_walk(tail(e))
                   : false;
        }
    }
    return tree_walk(expression);
}

4.4.4.5 维护数据库

设计逻辑编程语言的一个重要问题是安排事物,以便在检查给定模式时尽可能少地检查不相关的数据库条目。为此,我们将断言表示为一个列表,其头部是表示断言信息类型的字符串。我们将断言存储在单独的流中,每种信息类型一个流,在一个由信息类型索引的表中。要获取可能匹配模式的断言,我们返回(以便使用匹配器进行测试)所有具有相同头部(相同信息类型)的存储断言。更聪明的方法也可以利用帧中的信息。我们避免构建用于索引程序的标准;相反,我们调用体现我们标准的谓词和选择器。

function fetch_assertions(pattern, frame) {
    return get_indexed_assertions(pattern);
}
function get_indexed_assertions(pattern) {
    return get_stream(index_key_of(pattern), "assertion-stream");
}

get_stream函数在表中查找流,并在没有存储内容时返回一个空的流。

function get_stream(key1, key2) {
    const s = get(key1, key2);
    return is_undefined(s) ? null : s;
}

规则也是类似地存储,使用规则结论的头部。一个模式可以匹配具有相同头部的规则结论的规则。因此,当获取可能匹配模式的规则时,我们获取所有结论具有与模式相同头部的规则。

function fetch_rules(pattern, frame) {
    return get_indexed_rules(pattern);
}
function get_indexed_rules(pattern) {
    return get_stream(index_key_of(pattern), "rule-stream");
}

add_rule_or_assertion函数由query_driver_loop用于向数据库添加断言和规则。每个项目都存储在索引中。

function add_rule_or_assertion(assertion) {
    return is_rule(assertion)
           ? add_rule(assertion)
           : add_assertion(assertion);
}
function add_assertion(assertion) {
    store_assertion_in_index(assertion);
    return "ok";
}
function add_rule(rule) {
    store_rule_in_index(rule);
    return "ok";
}

要实际存储断言或规则,我们将其存储在适当的流中。

function store_assertion_in_index(assertion) {
    const key = index_key_of(assertion);
    const current_assertion_stream =
                get_stream(key, "assertion-stream");
    put(key, "assertion-stream",
        pair(assertion, () => current_assertion_stream));
}
function store_rule_in_index(rule) {
    const pattern = conclusion(rule);
    const key = index_key_of(pattern);
    const current_rule_stream =
                get_stream(key, "rule-stream");
    put(key, "rule-stream",
        pair(rule, () => current_rule_stream));
}

模式(断言或规则结论)存储在表中的键是它以字符串开头的字符串。

function index_key_of(pattern) { return head(pattern); }

4.4.4.6 流操作

查询系统使用了一些流操作,这些操作在第 3 章中没有介绍。stream_append_delayedinterleave_delayed函数与stream_appendinterleave(第 3.5.3 节)类似,只是它们接受了一个延迟参数(就像第 3.5.4 节中的integral函数)。这在某些情况下会推迟循环(参见练习 4.68)。

function stream_append_delayed(s1, delayed_s2) {
    return is_null(s1)
           ? delayed_s2()
           : pair(head(s1),
                  () => stream_append_delayed(stream_tail(s1),
                                              delayed_s2));
}
function interleave_delayed(s1, delayed_s2) {
    return is_null(s1)
           ? delayed_s2()
           : pair(head(s1),
                  () => interleave_delayed(delayed_s2(),
                                           () => stream_tail(s1)));
}

stream_flatmap函数在查询求值器中用于在帧流上映射函数并组合结果帧流,它是第 2.2.3 节中为普通列表引入的flatmap函数的流模拟。然而,与普通的flatmap不同,我们使用交错过程累积流,而不是简单地追加它们(参见练习 4.69 和 4.70)。

function stream_flatmap(fun, s) {
    return flatten_stream(stream_map(fun, s));
}
function flatten_stream(stream) {
    return is_null(stream)
           ? null
           : interleave_delayed(
                  head(stream),
                  () => flatten_stream(stream_tail(stream)));
}

求值器还使用以下简单函数生成由单个元素组成的流:

function singleton_stream(x) {
    return pair(x, () => null);
}

4.4.4.7 查询语法函数和实例化

我们在第 4.4.4.1 节中看到,驱动循环首先将输入字符串转换为 JavaScript 语法表示。输入被设计成看起来像 JavaScript 表达式,以便我们可以使用第 4.1.2 节中的parse函数,并且还支持javascript_predicate中的 JavaScript 表示。例如,

parse('job($x, list("computer", "wizard"));');

产生

list("application",
     list("name", "job"),
     list(list("name", "$x"),
          list("application",
               list("name", "list"),
               list(list("literal", "computer"),
                    list("literal", "wizard")))))

标签"application"表示,从语法上讲,查询将被视为 JavaScript 中的函数应用。函数unparse将语法转换回字符串:

unparse(parse('job($x, list("computer", "wizard"));'));
'job($x, list("computer", "wizard"))'

在查询处理器中,我们假设了断言、规则和查询的查询语言特定表示。函数convert_to_query_syntax将语法表示转换为该表示。使用相同的示例,

convert_to_query_syntax(parse('job($x, list("computer", "wizard"));'));

产生

list("job", list("name", "$x"), list("computer", "wizard"))

查询系统函数,如第 4.4.4.5 节中的add_rule_or_assertion和第 4.4.4.2 节中的evaluate_query,使用下面声明的选择器和谓词,如typecontentsis_rulefirst_conjunct,对查询语言特定表示进行操作。图 4.8 描述了查询系统使用的三个抽象屏障以及转换函数parseunparseconvert_to_query_syntax如何连接它们。

c4-fig-0008.jpg

图 4.8 查询系统中的语法抽象。

处理模式变量

在查询处理过程中,谓词is_variable用于查询语言特定表示,并在实例化过程中用于 JavaScript 语法表示,以识别以美元符号开头的名称。我们假设有一个char_at函数,它返回一个字符串,该字符串仅包含给定位置的给定字符串的字符。⁷⁵

function is_variable(exp) {
    return is_name(exp) && char_at(symbol_of_name(exp), 0) === "$";
}

唯一变量是通过规则应用(在 4.4.4.4 节)中的以下函数构建的。规则应用的唯一标识符是一个数字,每次应用规则时都会递增。⁷⁶

let rule_counter = 0;

function new_rule_application_id() { 
    rule_counter = rule_counter + 1;
    return rule_counter;
}
function make_new_variable(variable, rule_application_id) {
    return make_name(symbol_of_name(variable) + "_" +
                     stringify(rule_application_id));
}
函数convert_to_query_syntax

函数convert_to_query_syntax通过简化断言、规则和查询,将 JavaScript 语法表示递归转换为查询语言特定表示,使得应用程序的函数表达式中的名称的符号成为标记,除非该符号是"pair""list",则构建一个(未标记的)JavaScript 对或列表。这意味着convert_to_query_syntax在转换过程中解释构造函数pairlist的应用,并且处理函数(如 4.4.4.3 节的pattern_match和 4.4.4.4 节的unify_match)可以直接操作预期的对和列表,而不是在解析器生成的语法表示上进行操作。javascript_predicate的(单元素)“参数”列表保持未处理,如下所述。变量保持不变,文字简化为其包含的原始值。

function convert_to_query_syntax(exp) {
   if (is_application(exp)) {
     const function_symbol = symbol_of_name(function_expression(exp));
     if (function_symbol === "javascript_predicate") {
       return pair(function_symbol, arg_expressions(exp));
     } else {
       const processed_args = map(convert_to_query_syntax,
                                  arg_expressions(exp));
       return function_symbol === "pair"
              ? pair(head(processed_args), head(tail(processed_args)))
              : function_symbol === "list"
              ? processed_args
              : pair(function_symbol, processed_args);
     }
   } else if (is_variable(exp)) {
     return exp;
   } else { // exp is literal
     return literal_value(exp);
   }
}

对此处理的一个例外是javascript_predicate。由于其谓词表达式的实例化 JavaScript 语法表示被传递给 4.1.1 节的evaluate,来自parse的原始语法表示需要保持在表达式的查询语言特定表示中。在 4.4.1 节的这个例子中

and(salary($person, $amount), javascript_predicate($amount > 50000))

convert_to_query_syntax生成一个数据结构,其中 JavaScript 语法表示嵌入在查询语言特定表示中:

list("and",
     list("salary", list("name", "$person"), list("name", "$amount")),
     list("javascript_predicate",
          list("binary_operator_combination",
               ">",
               list("name", "$amount"),
               list("literal", 50000))))

为了求值已处理查询的javascript_predicate子表达式,4.4.4.2 节的函数javascript_predicate调用嵌入的 JavaScript 语法表示的$amount > 50000instantiate_expression(下文)来替换变量list("name", "$amount")为一个文字,例如list("literal", 70000),表示$amount绑定的原始值,这里是 70000。JavaScript 求值器可以求值实例化的谓词,现在表示为70000 > 50000

表达式的实例化

4.4.4.2 节的函数javascript_predicate和 4.4.4.1 节的驱动循环在表达式上调用instantiate_expression,以获得一个副本,其中表达式中的任何变量都被给定帧中的值替换。输入和结果表达式使用 JavaScript 语法表示,因此从实例化变量中得到的任何值都需要从其在绑定中的形式转换为 JavaScript 语法表示。

function instantiate_expression(expression, frame) {
   return is_variable(expression)
          ? convert(instantiate_term(expression, frame))
          : is_pair(expression)
          ? pair(instantiate_expression(head(expression), frame),
                 instantiate_expression(tail(expression), frame))
          : expression;
}

函数instantiate_term以变量、对或原始值作为第一个参数,并以帧作为第二个参数,并递归地将第一个参数中的变量替换为帧中的值,直到达到原始值或未绑定变量。当过程遇到对时,将构造一个新的对,其部分是原始部分的实例化版本。例如,如果在帧f中将$x绑定到对[$y, 5],并且$y又绑定到 3,那么将instantiate_term应用于list("name", "$x")f的结果是对[3, 5]。

function instantiate_term(term, frame) {
    if (is_variable(term)) {
        const binding = binding_in_frame(term, frame);
        return is_undefined(binding)
              ? term // leave unbound variable as is
           : instantiate_term(binding_value(binding), frame);
    } else if (is_pair(term)) {
        return pair(instantiate_term(head(term), frame),
                    instantiate_term(tail(term), frame));
    } else { // term is a primitive value
        return term;
    }
}

函数convert构造了一个 JavaScript 语法表示,用于instantiate_term返回的变量、对或原始值。原始中的一对变成了 JavaScript 的对构造函数的应用,原始值变成了文字。

function convert(term) {
    return is_variable(term)
           ? term
           : is_pair(term)
           ? make_application(make_name("pair"),
                              list(convert(head(term)),
                                   convert(tail(term))))
           : // term is a primitive value
             make_literal(term);
}

为了说明这三个函数,考虑查询时发生的情况

job($x, list("computer", "wizard"))

其 JavaScript 语法表示在第 4.4.4.7 节开头给出,由驱动循环处理。假设结果流的帧g将变量$x绑定到["Bitdiddle", $y],变量$y绑定到["Ben", null]

instantiate_term(list("name", "$x"), g)

返回列表

list("Bitdiddle", "Ben")

convert转换为

list("application",
     list("name", "pair"),
     list(list("literal", "Bitdiddle"),
          list("application",
               list("name", "pair"),
               list(list("literal", "Ben"),
                    list("literal", null)))))

应用于查询的 JavaScript 语法表示和帧ginstantiate_expression的结果是:

list("application",
     list("name", "job"),
     list(list("application",
               list("name", "pair"),
               list(list("literal", "Bitdiddle"),
                    list("application",
                         list("name", "pair"),
                         list(list("literal", "Ben"),
                              list("literal", null))))),
          list("application",
               list("name", "list"),
               list(list("literal", "computer"),
                    list("literal", "wizard")))))

驱动循环取消解析此表示并将其显示为:

'job(list("Bitdiddle", "Ben"), list("computer", "wizard"))'
函数unparse

函数unparse通过应用第 4.1.2 节的语法规则将给定的 JavaScript 语法表示转换为字符串。我们仅描述了unparse用于第 4.4.1 节示例中出现的那些表达式的类型,将语句和其余类型的表达式留给练习 4.2。通过stringifying其值来转换文字,将名称转换为其符号。应用通过取消解析函数表达式格式化,我们可以假定这里是一个名称,后面跟着用括号括起来的逗号分隔的参数表达式字符串。二元运算符组合使用中缀表示法格式化。

function unparse(exp) {
    return is_literal(exp)
           ? stringify(literal_value(exp))
           : is_name(exp)
           ? symbol_of_name(exp)
           : is_list_construction(exp)
           ? unparse(make_application(make_name("list"),
                                      element_expressions(exp)))
           : is_application(exp) && is_name(function_expression(exp))
           ? symbol_of_name(function_expression(exp)) +
                 "(" +
                 comma_separated(map(unparse, arg_expressions(exp))) +
                 ")"
           : is_binary_operator_combination(exp)
           ? "(" + unparse(first_operand(exp)) +
             " " + operator_symbol(exp) +
             " " + unparse(second_operand(exp)) +
             ")"
           〈unparsing other kinds of JavaScript components〉
           : error(exp, "unknown syntax – unparse");
}
function comma_separated(strings) {
    return accumulate((s, acc) => s + (acc === "" ? "" : ", " + acc),
                      "",
                      strings);
}

函数unparse可以在没有子句的情况下正常工作

: is_list_construction(exp)
? unparse(make_application(make_name("list"),
                           element_expressions(exp)))

但在模式变量由列表实例化的情况下,输出字符串将是不必要冗长的。在上面的例子中,处理查询

job($x, list("computer", "wizard"))

产生将$x绑定到["Bitdiddle", ["Ben", null]]的帧,unparse产生

'job(list("Bitdiddle", "Ben"), list("computer", "wizard"))'

但是,如果没有子句,它将产生

'job(pair("Bitdiddle", pair("Ben", null)), list("computer", "wizard"))'

明确构造了构成第一个列表的两个对。为了实现第 4.4.1 节中使用的更简洁的格式,我们插入了一个子句来检查表达式是否构造了一个列表,如果是的话,我们将其格式化为list的单个应用,该应用是我们从表达式中提取的元素表达式的列表。列表构造是文字nullpair的应用,其第二个参数本身是列表构造。

function is_list_construction(exp) {
    return (is_literal(exp) && is_null(literal_value(exp))) ||
           (is_application(exp) && is_name(function_expression(exp)) &&
            symbol_of_name(function_expression(exp)) === "pair" &&
            is_list_construction(head(tail(arg_expressions(exp)))));
}

从给定的列表构造中提取元素表达式相当于收集pair的应用的第一个参数,直到达到文字null

function element_expressions(list_constr) {
    return is_literal(list_constr)
           ? null // list_constr is literal null
           :      // list_constr is application of pair
             pair(head(arg_expressions(list_constr)),
                  element_expressions(
                      head(tail(arg_expressions(list_constr)))));
}
查询语言特定表示的谓词和选择器

函数typecontents,由evaluate_query(第 4.4.4.2 节)使用,指定了查询语言特定表示的句法形式由其头部的字符串标识。它们与第 2.4.2 节中的type_tagcontents函数相同,只是错误消息不同。

function type(exp) {
    return is_pair(exp)
           ? head(exp)
           : error(exp, "unknown expression type");
}
function contents(exp) {
    return is_pair(exp)
           ? tail(exp)
           : error(exp, "unknown expression contents");
}

以下函数由query_driver_loop(第 4.4.4.1 节)使用,指定规则和断言通过assert命令添加到数据库中,函数convert_to_query_syntax将其转换为形式的一对["assert", rule-or-assertion]

function is_assertion(exp) {
    return type(exp) === "assert";
}
function assertion_body(exp) { return head(contents(exp)); }

以下是andornotjavascript_predicate语法形式(第 4.4.4.2 节)的谓词和选择器的声明:

function is_empty_conjunction(exps) { return is_null(exps); }
function first_conjunct(exps) { return head(exps); }
function rest_conjuncts(exps) { return tail(exps); }

function is_empty_disjunction(exps) { return is_null(exps); }
function first_disjunct(exps) { return head(exps); }
function rest_disjuncts(exps) { return tail(exps); }

function negated_query(exps) { return head(exps); }

function javascript_predicate_expression(exps) { return head(exps); }

以下三个函数定义了查询语言特定规则的表示:

function is_rule(assertion) {
    return is_tagged_list(assertion, "rule");
}
function conclusion(rule) { return head(tail(rule)); }
function rule_body(rule) {
    return is_null(tail(tail(rule)))
           ? list("always_true")
           : head(tail(tail(rule)));
}

4.4.4.8 帧和绑定

帧被表示为绑定的列表,绑定是变量-值对:

function make_binding(variable, value) {
    return pair(variable, value);
}
function binding_variable(binding) {
    return head(binding);
}
function binding_value(binding) {
    return tail(binding);
}
function binding_in_frame(variable, frame) {
    return assoc(variable, frame);
}
function extend(variable, value, frame) {
    return pair(make_binding(variable, value), frame);
}
练习 4.68

路易斯·里森纳想知道为什么simple_querydisjoin函数(第 4.4.4.2 节)是使用延迟表达式实现的,而不是定义如下:

function simple_query(query_pattern, frame_stream) {
    return stream_flatmap(
               frame =>
                 stream_append(find_assertions(query_pattern, frame),
                               apply_rules(query_pattern, frame)),
               frame_stream);
}
function disjoin(disjuncts, frame_stream) {
    return is_empty_disjunction(disjuncts)
           ? null
           : interleave(
                  evaluate_query(first_disjunct(disjuncts), frame_stream),
                  disjoin(rest_disjuncts(disjuncts), frame_stream));
}

您能举例说明这些更简单的定义会导致不良行为的查询吗?

练习 4.69

为什么disjoinstream_flatmap交错流而不是简单地附加它们?给出说明交错效果更好的例子。 (提示:为什么我们在 3.5.3 节中使用interleave?)

练习 4.70

flatten_stream为什么在其主体中使用延迟表达式?以下定义它会有什么问题:

function flatten_stream(stream) {
    return is_null(stream)
           ? null
           : interleave(head(stream),
                        flatten_stream(stream_tail(stream)));
}
练习 4.71

Alyssa P. Hacker 建议在negatejavascript_predicatefind_assertions中使用stream_flatmap的简化版本。她观察到在这些情况下映射到帧流中的函数总是产生空流或单例流,因此在组合这些流时不需要交错。

  1. a. 填写 Alyssa 程序中的缺失表达式。

    function simple_stream_flatmap(fun, s) {
        return simple_flatten(stream_map(fun, s));
    }
    function simple_flatten(stream) {
        return stream_map(〈??〉,
                          stream_filter(〈??〉, stream));
    }
    
  2. b. 如果我们以这种方式改变它,查询系统的行为会改变吗?

练习 4.72

为查询语言实现一个名为unique的语法形式。unique的应用应该成功,如果数据库中满足指定查询的项目恰好有一个。例如,

unique(job($x, list("computer", "wizard")))

应该打印一个项目流

unique(job(list("Bitdiddle", "Ben"), list("computer", "wizard")))

由于 Ben 是唯一的计算机巫师,而

unique(job($x, list("computer", "programmer")))

应该打印空流,因为有不止一个计算机程序员。此外,

and(job($x, $j), unique(job($anyone, $j)))

应列出所有只由一个人填写的工作以及填写它们的人。

实现unique有两个部分。第一部分是编写处理这种语法形式的函数,第二部分是使evaluate_query分派到该函数。第二部分是微不足道的,因为evaluate_query以数据导向的方式进行分派。如果您的函数被称为uniquely_asserted,您只需要

put("unique", "evaluate_query", uniquely_asserted);

evaluate_query将为每个type(头)为字符串"unique"的查询分派到此函数。

真正的问题是编写函数uniquely_asserted。这应该将unique查询的contents(尾部)作为输入,以及一系列帧的流。对于流中的每个帧,它应该使用evaluate_query来查找满足给定查询的所有扩展帧的流。任何流中不恰好有一个项目的流都应该被消除。剩下的流应该被传回来累积成一个大流,这是unique查询的结果。这类似于not语法形式的实现。

通过形成一个列出监督恰好一个人的所有人的查询来测试您的实现。

练习 4.73

我们将and的实现作为查询的系列组合(图 4.6)是优雅的,但效率低下,因为在处理and的第二个查询时,我们必须为第一个查询产生的每个帧扫描数据库。如果数据库有N个元素,并且典型查询产生的输出帧数量与N成比例(比如N / k),那么为了处理第一个查询产生的每个帧,需要N²/k次模式匹配器调用。另一种方法是分别处理and的两个子句,然后寻找所有兼容的输出帧对。如果每个查询产生N / k个输出帧,那么这意味着我们必须执行N²/k²个兼容性检查——比我们当前方法所需的匹配次数少k倍。

设计一个使用这种策略的and的实现。您必须实现一个函数,该函数以两个帧作为输入,检查帧中的绑定是否兼容,如果是,则生成一个合并两组绑定的帧。此操作类似于统一。

练习 4.74

在第 4.4.3 节中,我们看到notjavascript_predicate可能会导致查询语言在应用到变量未绑定的帧时给出“错误”的答案。想出一种修复这个缺点的方法。一个想法是通过向帧附加一个“承诺”来进行“延迟”过滤,只有当足够的变量被绑定时才能实现过滤操作。我们可以等到执行所有其他操作之后再执行过滤。然而,出于效率的考虑,我们希望尽快进行过滤,以减少生成的中间帧的数量。

练习 4.75

将查询语言重新设计为一个非确定性程序,以使用第 4.3 节的求值器来实现,而不是作为一个流程过程。在这种方法中,每个查询将产生一个单一的答案(而不是所有答案的流),用户可以输入retry来查看更多答案。你会发现,我们在本节构建的许多机制都被非确定性搜索和回溯所包含。然而,你可能也会发现,你的新查询语言在行为上有微妙的差异。你能找到一些例子来说明这种差异吗?

练习 4.76

当我们在第 4.1 节实现 JavaScript 求值器时,我们看到了如何使用局部环境来避免函数参数之间的名称冲突。例如,在求值中

function square(x) {
    return x * x;
}
function sum_of_squares(x, y) {
    return square(x) + square(y);
}
sum_of_squares(3, 4);

squaresum_of_squares中的x之间没有混淆,因为我们在一个特别构造的环境中求值每个函数的主体,该环境包含局部名称的绑定。在查询系统中,我们使用了一种不同的策略来避免在应用规则时出现名称冲突。每次应用规则时,我们都会使用新的名称对变量进行重命名,这些名称保证是唯一的。对于 JavaScript 求值器的类似策略将是放弃局部环境,而是在应用函数时每次重命名函数的主体中的变量。

为查询语言实现一个使用环境而不是重命名的规则应用方法。看看你是否可以在你的环境结构上构建,以创建处理大型系统的查询语言构造,比如块结构函数的规则类比。你能把这些与在特定上下文中进行推理(例如,“如果我假设P是真的,那么我就能推断出AB。”)作为解决问题的方法联系起来吗?(这个问题是开放式的。)