有几个问题。以下是最明显的:
打字。
您的谓词re_contains/2
需要一个列表作为第二个参数。成功是re_contains(a,a).
巧合而非意图。
单调性。另一个问题是re_contains([a],[a])
成功了,但re_contains([X],[a])
失败了。或者,换个角度看:re_contains([X],[a])
失败,但X = a, re_contains([X],[a])
成功。通过添加目标X = a
,我们专门化了查询,因此最初失败的查询应该再次失败。
身份测试 ( ==/2
) 应替换为相等 ( =/2
)并确保我们有一些列表。所以:
re_contains(X, L) :-
% X == L。
X = L,
附加(X,_,_)。
请注意,append/3
此处仅用于确保 X 是一个列表 - 不使用实际的附加功能。
效率。第三个问题涉及您如何表示串联的实际方式。让我们看看以下规则:
re_contains(seq(X, Y), L) :-
附加(L1,L2,L),
re_contains(X, L1),
重新包含(Y,L2)。
现在,假设我们这里有一个长度列表N
。目标可能有多少个答案append(L1, L2, L)
?其实N + 1
。而这一点,不管所涉及的实际值如何。现在考虑:
?- 长度(L,1000000), 时间(re_contains(seq([a],[]),[b|L]))。
% 2,000,005 次推理,0.886 CPU 在 0.890 秒内(100% CPU,2258604 唇)
错误的。
re_contains/2
这里需要的时间与列表的长度成正比。但是只要看看第一个元素就足以意识到这是不可能的。
所以背后的问题是使用append/3
. Prolog 有一个简单的经验法则:如果您使用过多,请append/3
考虑使用dcg s — 定从句语法。请查看标签以获取更多详细信息 - 并查阅介绍性 Prolog 文本。为了让您开始,这里是您定义的一个子集:
re_contains(RE, L) :-
短语(重新(RE),L)。
重新([])-> []。
重新([E])-> [E]。
重新(序列(X,Y))-->
重新(X),
重新(Y)。
不再调查整个列表:
?-长度(L,1000000),时间(短语(re(seq([a],[])),[b|L]))。
% 6 次推理,0.000 秒内 0.000 CPU(88% CPU,127313 唇)
错误的。
顺便说一句,这是一个完整的定义。
终止/不终止。与效率相关的是终止属性。理想情况下,如果可以有限地表示一组解决方案,则查询将终止。也就是说,通过有限数量的答案。好的,这就是我们正在努力的理想。Prolog 的简单但非常有效的执行算法有时会循环,当可能有有限数量的答案时。理解不终止的原因有时非常棘手。通常的调试策略——比如使用调试器进行跟踪或单步调试——不起作用,因为它们向您展示了太多细节。幸运的是,有一种更好的技术:
我不会再看你的整个程序,而只会看它的一小部分。该片段是一个故障切片(有关更多详细信息,请参见链接)。它小得多,但讲述了很多关于原始程序的信息——前提是它是一个纯粹的、单调的程序:
如果失败切片没有终止,则原始程序不会终止。
因此,如果我们找到这样的失败片段,我们可以立即对整个程序做出结论。甚至没有阅读其余部分!
这是一个有趣的失败片段:
...
re_contains(X, L) :- false,
X = L
re_contains(seq(X, Y), L) :-
附加(L1,L2,L),假,
re_contains(X,L1),
re_contains(Y,L2)。
re_contains(union(X, _), L) :- false,
re_contains(X, L)。
...
现在考虑目标re_contains(seq([],[]),L).
理想情况下,应该只有一个答案L = []
并且目标应该终止。但是,它会循环,因为append(L1, L2, L)
不会终止。将此与上面终止的 DCG 解决方案进行对比phrase(re(seq([],[])),L)
。