什么是 PL-Unit 中的 "Test succeeded with choicepoint" 警告,我该如何修复它?

What is a "Test succeeded with choicepoint" warning in PL-Unit, and how do I fix it?

我正在编写序言程序来检查变量是否为整数。 我的方式 "returning" 结果很奇怪,但我认为这对回答我的问题并不重要。

测试

我已经为这种行为编写了通过单元测试;他们在这里...

foo_test.pl

:- begin_tests('foo').
:- consult('foo').

test('that_1_is_recognised_as_int') :-
    count_ints(1, 1).

test('that_atom_is_not_recognised_as_int') :-
    count_ints(arbitrary, 0).

:- end_tests('foo').
:- run_tests.

代码

下面是通过这些测试的代码...

foo.pl

count_ints(X, Answer) :-
  integer(X),
  Answer is 1.

count_ints(X, Answer) :-
  \+ integer(X),
  Answer is 0.

输出

测试通过了,这很好,但是当我 运行 时,我 收到警告 。这是 运行 测试时的输出...

?- ['foo_test'].
%  foo compiled into plunit_foo 0.00 sec, 3 clauses
% PL-Unit: foo 
Warning: /home/brandon/projects/sillybin/prolog/foo_test.pl:11:
        /home/brandon/projects/sillybin/prolog/foo_test.pl:4:
        PL-Unit: Test that_1_is_recognised_as_int: Test succeeded with choicepoint
. done
% All 2 tests passed
% foo_test compiled 0.03 sec, 1,848 clauses
true.

问题

首先,让我们忘记整个测试框架,只考虑顶层的查询:

?- count_ints(1, 1).
true ;
false.

这个交互告诉你,在第一个解决方案之后,还剩下一个选择点。这意味着 备选方案 有待尝试,并且在回溯时进行了尝试。在这种情况下,没有进一步的解决方案,但系统在实际尝试之前无法说明这一点。

对测试用例使用 all/1 选项

有多种方法可以修复警告。一个直截了当的方法是像这样陈述测试用例:

test('that_1_is_recognised_as_int', all(Count = [1])) :-
    count_ints(1, Count).

这会隐式收集所有解决方案,然后立即对所有解决方案进行声明。

使用 if-then-else

一个更智能的解决方案是使 count_ints/2 本身具有确定性!

一种方法是使用 if-then-else,如下所示:

count_ints(X, Answer) :-
        (   integer(X) -> Answer = 1
        ;   Answer = 0
        ).

我们现在有:

?- count_ints(1, 1).
true.

也就是说,查询现在确定性地成功了。

纯粹的解决方案:清理数据结构

但是,最优雅的解决方案是使用干净的表示,这样您和 Prolog 引擎就可以区分所有情况模式匹配.

例如,我们 可以 将整数表示为 i(N),将其他所有内容表示为 other(T).

在这种情况下,我使用包装器 i/1other/1 来区分大小写。

现在我们有:

count_ints(i(_), 1).
count_ints(other(_), 0).

测试用例可能如下所示:

test('that_1_is_recognised_as_int') :-
    count_ints(i(1), 1).

test('that_atom_is_not_recognised_as_int') :-
    count_ints(other(arbitrary), 0).

这也可以在没有警告的情况下运行,具有代码实际可用于生成答案的显着优势:

?- count_ints(Term, Count).
Term = i(_1900),
Count = 1 ;
Term = other(_1900),
Count = 0.

与其他版本相比:

?- count_ints(Term, Count).
Count = 0.

不幸的是,最多只能考虑涵盖 50% 的可能情况...

更严格的限制

正如 Boris 在评论中正确指出的那样,我们可以通过 约束 i/1 项的参数为 整数 。比如我们可以这样写:

count_ints(i(I), 1) :- I in inf..sup.
count_ints(other(_), 0).

现在,参数 必须是 一个整数,这可以通过以下查询变得清晰:

?- count_ints(X, 1).
X = i(_1820),
_1820 in inf..sup.

?- count_ints(i(any), 1).
ERROR: Type error: `integer' expected, found `any' (an atom)

请注意,Boris 提到的示例在没有此类更严格约束的情况下也失败了:

?- count_ints(X, 1), X = anything.
false.

尽管如此,在参数上添加更多约束通常很有用,如果您需要对整数进行推理,CLP(FD) 约束通常是显式声明类型约束的一个很好的通用解决方案,否则这些约束仅隐含在你的程序。

注意integer/1没有收到备忘录:

?- X in inf..sup, integer(X).
false.

由此可见,虽然X在本例中毫无疑问地被约束为整数,但integer(X)仍然没有成功。因此,您不能使用 integer/1 等谓词作为可靠的类型检测器。依靠模式匹配和使用约束来增加程序的通用性要好得多。

要事第一:documentation of the SWI-Prolog Prolog Unit Tests package is quite good. The different modes are explained in Section 2.2. Writing the test body。 2.2.1中的相关语句是:

Deterministic predicates are predicates that must succeed exactly once and, for well behaved predicates, leave no choicepoints. [emphasis mine]

什么是选择点?

在过程编程中,当你调用一个函数时,它可以return一个值,或一组值;它可以修改状态(本地或全局);不管它做什么,它只会做一次。

在 Prolog 中,当您评估谓词时,会在证明树中搜索解决方案。解决方案可能不止一种!假设您这样使用 between/3

For x = 1, is x in [0, 1, 2]?

?- between(0, 2, 1).
true.

不过你也可以问:

Enumerate all x such that x is in [0, 1, 2].

?- between(0, 2, X).
X = 0 ;
X = 1 ;
X = 2.

得到第一个解X = 0后,Prolog停止等待;这意味着:

The query between(0, 2, X) has at least one solution, X = 0. It might have further solutions; press ; and Prolog will search the proof tree for the next solution.

选择点是Prolog在找到解后放在搜索树中的标记。它将继续搜索该标记的下一个解决方案。

警告"Test succeeded with choicepoint"表示:

The solution Prolog found was the solution the test expected; however, there it leaves behind a choice point, so it is not "well-behaved".

选择点有问题吗?

你没有故意放在那里的选择点可能是个问题。在不深入细节的情况下,它们可以阻止某些优化并导致效率低下。这没关系,但有时只有第一个解决方案是您(程序员)想要的解决方案,而下一个解决方案可能会产生误导或错误。或者,著名的是,在给你一个有用的答案之后,Prolog 可以进入无限循环。

同样,如果您知道这一点,那很好:在评估此谓词时,您永远不会要求多个解决方案。你可以把它包裹在 once/1 中,像这样:

?- once( between(0, 2, X) ).

?- once( count_ints(X, Answer) ).

如果其他人使用了您的代码,尽管一切皆有可能。成功选择点可能意味着从 "there are other useful solutions" 到 "no more solutions, this will now fail" 到 "other solutions, but not the kind you wanted" 到 "going into an infinite loop now!"

去掉选择点

举个具体的例子:你有一个built-in,integer/1,它会成功或失败而不留下选择点。因此,count_ints/2 的原始定义中的这两个子句对于 Xany 值是互斥的:

count_ints(X, Answer) :-
  integer(X), ...

count_ints(X, Answer) :-
  \+ integer(X), ...

然而,Prolog 并不知道这一点。它只查看子句 heads 并且这两个是相同的:

count_ints(X, Answer) :- ...

count_ints(X, Answer) :- ...

两个头是相同的,Prolog 不会进一步查看子句头来决定另一个子句是否值得尝试,所以它会尝试第二个子句,即使第一个参数确实是一个整数(这是您收到的警告中的 "choice point"),并且 总是失败

因为您知道这两个子句是互斥的,所以告诉 Prolog 忘记另一个子句是安全的。您可以使用 once/1,如上所示。当第一个参数确实是整数时,您还可以削减证明树的其余部分:

count_ints(X, 1) :- integer(X), !.
count_ints(_, 0).

完全相同的操作语义,但 Prolog 编译器可能更容易优化:

count_ints(X, Answer) :-
    (   integer(X)
    ->  Answer = 1
    ;   Answer = 0
    ).

... 就像 mat 的回答一样。至于使用模式匹配,这一切都很好,但是如果 X 来自其他地方,而不是来自您自己编写的代码,您仍然需要在某个时候进行此检查。你最终会得到类似的东西:

variable_tagged(X, T) :-
    (   integer(X) -> T = i(X)
    ;   float(X)   -> T = f(X)
    ;   atom(X)    -> T = a(X)
    ;   var(X)     -> T = v(X)
    % and so on
    ;   T = other(X)
    ).

此时您可以按照 mat 的建议编写 count_ints/2,Prolog 将通过查看子句标题知道您的两个子句是互斥的。

once asked a question 归结为相同的 Prolog 行为以及如何处理它。垫的答案推荐相同的方法。 mat 对我在答案下方的评论的评论与答案本身一样重要(如果你至少在编写真正的程序)。