什么是 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.
- 我正在使用 SWI-Prolog(多线程,64 位,版本 6.6.6)
- 我尝试使用
;
将两个 count_ints
谓词合并为一个,但它仍然产生相同的警告。
- 我在使用 Debian 8(我怀疑它有什么不同)。
问题
- 这个警告是什么意思?还有...
- 我该如何预防?
首先,让我们忘记整个测试框架,只考虑顶层的查询:
?- 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
等谓词作为可靠的类型检测器。依靠模式匹配和使用约束来增加程序的通用性要好得多。
要事第一: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
的原始定义中的这两个子句对于 X
的 any 值是互斥的:
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 对我在答案下方的评论的评论与答案本身一样重要(如果你至少在编写真正的程序)。
我正在编写序言程序来检查变量是否为整数。 我的方式 "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.
- 我正在使用 SWI-Prolog(多线程,64 位,版本 6.6.6)
- 我尝试使用
;
将两个count_ints
谓词合并为一个,但它仍然产生相同的警告。 - 我在使用 Debian 8(我怀疑它有什么不同)。
问题
- 这个警告是什么意思?还有...
- 我该如何预防?
首先,让我们忘记整个测试框架,只考虑顶层的查询:
?- 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
等谓词作为可靠的类型检测器。依靠模式匹配和使用约束来增加程序的通用性要好得多。
要事第一: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
的原始定义中的这两个子句对于 X
的 any 值是互斥的:
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 对我在答案下方的评论的评论与答案本身一样重要(如果你至少在编写真正的程序)。