什么是PL-Unit中的"测试成功选择点"警告,我该如何解决?

byx*_*xor 3 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.
Run Code Online (Sandbox Code Playgroud)

代码

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

foo.pl

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

count_ints(X, Answer) :-
  \+ integer(X),
  Answer is 0.
Run Code Online (Sandbox Code Playgroud)

输出

测试正在通过,这很好,但我在运行它时会收到警告.这是运行测试时的输出...

?- ['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.
Run Code Online (Sandbox Code Playgroud)
  • 我正在使用SWI-Prolog(多线程,64位,版本6.6.6)
  • 我尝试将两个count_ints谓词组合成一个,使用;,但仍然会产生相同的警告.
  • 我在Debian 8上(我怀疑它有所不同).

问题

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

mat*_*mat 7

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

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


小智 7

首先要做的是:SWI-Prolog Prolog 单元测试包文档非常好。不同的模式在第 2.2 节中解释编写测试体。2.2.1中的相关语句是:

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

什么是选择点?

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

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

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

?- between(0, 2, 1).
true.
Run Code Online (Sandbox Code Playgroud)

但你也可以问:

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

?- between(0, 2, X).
X = 0 ;
X = 1 ;
X = 2.
Run Code Online (Sandbox Code Playgroud)

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

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

选择点是把序言搜索树中找到解决办法后的标志。它将继续从该标记搜索下一个解决方案。

警告“选择点测试成功”意味着:

Prolog 找到的解决方案是测试预期的解决方案;然而,它在那里留下了一个选择点,所以它不是“乖巧”。

选择点有问题吗?

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

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

?- once( between(0, 2, X) ).
Run Code Online (Sandbox Code Playgroud)

或者

?- once( count_ints(X, Answer) ).
Run Code Online (Sandbox Code Playgroud)

如果其他人使用您的代码,尽管所有赌注都已关闭。在选择点上取得成功可能意味着从“还有其他有用的解决方案”到“没有更多解决方案,现在将失败”到“其他解决方案,但不是您想要的那种”再到“现在进入无限循环!”

摆脱选择点

对于特定示例:您有一个内置的integer/1,它会成功或失败而不会留下选择点。因此,您原始定义中的这两个子句count_ints/2对于 的任何值都是互斥的X

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

count_ints(X, Answer) :-
  \+ integer(X), ...
Run Code Online (Sandbox Code Playgroud)

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

count_ints(X, Answer) :- ...

count_ints(X, Answer) :- ...
Run Code Online (Sandbox Code Playgroud)

这两个头是相同的,Prolog 没有进一步看子句头来决定另一个子句是否值得尝试,所以即使第一个参数确实是一个整数,它也会尝试第二个子句(这是“选择点”在您收到的警告中),并且总是失败

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

count_ints(X, 1) :- integer(X), !.
count_ints(_, 0).
Run Code Online (Sandbox Code Playgroud)

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

count_ints(X, Answer) :-
    (   integer(X)
    ->  Answer = 1
    ;   Answer = 0
    ).
Run Code Online (Sandbox Code Playgroud)

......就像垫子的答案一样。至于使用模式匹配,这一切都很好,但是如果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)
    ).
Run Code Online (Sandbox Code Playgroud)

在这一点上,您可以count_ints/2按照 mat 的建议编写您的内容,Prolog 将通过查看子句标题知道您的两个子句是互斥的。

曾经问过一个问题,归结为相同的 Prolog 行为以及如何处理它。mat 的答案推荐了相同的方法。mat 对答案下方我的评论的评论与答案本身一样重要(至少如果您正在编写真正的程序)。

  • @BrandonIbbotson 我的更好,显然 ;-) 另一个答案是由在 Prolog 中拥有比我更多的理论和实践经验的人写的。其实我是在看了另一个回答后才回答的,希望能“填空”,有很多经验的人在谈论一个他们非常熟悉的主题时必然会离开。在这种情况下,我认为接受一个不会真正降低另一个的可见性,因为无论如何只有两个答案。 (2认同)