有几个问题。以下是最明显的:
打字。
您的谓词 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,请考虑使用 dcgs — 定从句语法。请查看标签以获取更多详细信息 - 并查阅介绍性 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 唇)
错误的。
顺便说一句,here 是一个完整的定义。
终止/非终止。 与效率相关的是终止的属性。理想情况下,如果可以有限地表示一组解决方案,则查询将终止。也就是说,通过有限数量的答案。好的,这就是我们正在努力的理想。 Prolog 的简单但非常有效的执行算法有时会循环,当可能有有限数量的答案时。理解不终止的原因有时非常棘手。通常的调试策略——比如使用调试器进行跟踪或单步调试——不起作用,因为它们向您展示了太多细节。幸运的是,有一种更好的技术:
我不会再看你的整个程序,而只会看它的一小部分。这个片段是一个failure slice(更多细节参见链接)。它要小得多,但说明了很多关于原始程序的信息——前提是它是一个纯粹的、单调的程序:
如果失败切片没有终止,则原始程序不会终止。
因此,如果我们发现这样一个失败片段,我们可以立即对整个程序做出结论。甚至没有阅读其余部分!
这是一个有趣的失败片段:
...
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) 终止。