什么是PL-Unit中的“测试成功选择点”警告,我该如何修复它?

时间:2016-11-21 02:29:11

标签: unit-testing prolog plunit

我正在编写一个prolog程序来检查变量是否为整数。 我“回归”结果的方式很奇怪,但我认为回答我的问题并不重要。

测试

我已经为这种行为写了传递单元测试;他们在这......

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.
  • 我正在使用SWI-Prolog(多线程,64位,版本6.6.6)
  • 我尝试使用count_ints将两个;谓词合并为一个,但它仍会产生相同的警告。
  • 我在使用Debian 8(我怀疑它有所不同)。

问题

  • 这个警告意味着什么?而且......
  • 我该如何预防?

2 个答案:

答案 0 :(得分:6)

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

?- 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等谓词作为类型的可靠检测器。依靠模式匹配和使用约束来提高程序的通用性要好得多。

答案 1 :(得分:2)

首先要做的事情是:documentation of the SWI-Prolog Prolog Unit Tests package非常好。 Section 2.2. Writing the test body中解释了不同的模式。 2.2.1中的相关句子是:

  

确定性谓词是必须成功一次的谓词,对于表现良好的谓词,不留任何选择。 [强调我的]

什么是选择点?

在程序编程中,当你调用一个函数时,它可以返回一个值或一组值;它可以修改状态(本地或全局);无论它做什么,它都会完成一次。

在Prolog中,当您评估谓词时,会搜索证明树以寻找解决方案。可能有多个解决方案!假设你像这样使用between/3

  

对于 x = 1,在[0,1,2]中是 x 吗?

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

但你也可以问:

  

枚举所有 x ,使 x 在[0,1,2]中。

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

获得第一个解决方案X = 0后,Prolog停止并等待;这意味着:

  

查询between(0, 2, X)至少有一个解决方案X = 0。它可能还有其他解决方案;按;,Prolog将在证明树中搜索下一个解决方案。

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

警告"测试成功与选择点"的意思是:

  

Prolog发现的解决方案是测试预期的解决方案;然而,它留下了一个选择点,所以它不是"表现良好"。

选择点是否有问题?

你没有故意放在那里的选择点可能是个问题。没有详细说明,它们可以阻止某些优化并造成效率低下。这有点好,但有时只有第一个解决方案是你(程序员)想要的解决方案,而下一个解决方案可能会产生误导或错误。或者,着名的是,在给出一个有用的答案后,Prolog可以进入无限循环。

同样,如果您知道这一点,这很好:在评估此谓词时,您永远不会要求多个解决方案。您可以将其包装在once/1中,如下所示:

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

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

如果其他人使用您的代码,但所有投注都已关闭。成功选择点可能意味着来自"还有其他有用的解决方案"没有更多的解决方案,这将失败"到其他解决方案,但不是你想要的那种"现在进入一个无限循环!"

摆脱选择点

对于特定示例:您有一个内置的integer/1,无需离开选择点即可成功或失败。因此,count_ints/2原始定义中的这两个条款与X任何值互斥:

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

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

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

count_ints(X, Answer) :- ...

count_ints(X, Answer) :- ...

这两个脑袋是相同的,Prolog并不认为该子句会决定另一个子句是否值得尝试,所以即使第一个参数确实是一个整数,它也会尝试第二个子句(这是"选择点"在您收到的警告中),总是失败

既然您知道这两个条款是互斥的,那么告诉Prolog忘记其他条款是安全的。您可以使用once/1,如上所示。当第一个参数确实是一个整数时,您还可以剪切证明树的其余部分:

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

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

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

......就像席子里的回答一样。至于使用模式匹配,这一切都很好,但如果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的答案推荐采用相同的方法。我在答案下面的评论中的评论与答案本身一样重要(如果你至少写了真正的程序)。