使用手动列表迭代与通过失败递归的优缺点是什么

时间:2012-03-16 21:17:37

标签: prolog prolog-dif prolog-toplevel

我一直反对这一点,我无法确定攻击它的方法。以下是处理某些季节事实的两种方法。

我想弄清楚的是,是否使用方法1或方法2,以及每种方法的优缺点,尤其是大量事实。

methodone似乎很浪费,因为事实是可用的,为什么还要建立一个列表(特别是一个大的列表)。如果列表足够大,这也必须有内存含义吗?它没有利用Prolog的自然回溯功能。

methodtwo利用回溯来为我做递归,我猜想会有更多的内存效率,但通常这样做是很好的编程习惯吗?可以说可能会更难以理解,可能还有其他副作用吗?

我可以看到的一个问题是,每次调用fail时,我们都无法将任何内容传递回调用谓词,例如。如果它是methodtwo(SeasonResults),因为我们故意不断地使谓词失败。所以methodtwo需要断言事实来存储状态。

大概(?)方法2会更快,因为它没有(大)列表处理吗?

我可以想象,如果我有一个列表,那么methodone将是可行的方式......还是总是如此?在任何条件下使用methodone将列表断言为事实然后使用方法二处理它们是否有意义?完全疯了吗?

但话又说回来,我认为断言事实是一项非常“昂贵”的事情,所以即使对于大型名单,列表处理也可能是最佳选择?

有什么想法?或者有时候使用一个而不是另一个更好,这取决于(什么)情况?例如。对于内存优化,使用方法2,包括断言事实,以及速度使用方法1?

season(spring).
season(summer).
season(autumn).
season(winter).

 % Season handling
showseason(Season) :-
    atom_length(Season, LenSeason),
    write('Season Length is '), write(LenSeason), nl.

% -------------------------------------------------------------
% Method 1 - Findall facts/iterate through the list and process each
%--------------------------------------------------------------
% Iterate manually through a season list
lenseason([]).
lenseason([Season|MoreSeasons]) :-
    showseason(Season),
    lenseason(MoreSeasons).


% Findall to build a list then iterate until all done
methodone :-
    findall(Season, season(Season), AllSeasons),
    lenseason(AllSeasons),
    write('Done').

% -------------------------------------------------------------
% Method 2 - Use fail to force recursion
%--------------------------------------------------------------
methodtwo :-
    % Get one season and show it
    season(Season),
    showseason(Season),

    % Force prolog to backtrack to find another season
    fail.

% No more seasons, we have finished
methodtwo :-
    write('Done').

3 个答案:

答案 0 :(得分:10)

让我们看看你的例子。这很简单,所以我们会想象它更复杂。然而,似乎你认为副作用是必不可少的。让我质疑一下:

在你的例子中,你发现了一个非常有趣的发现:所有季节的名字长度相同。真是一个惊天动地的洞察力!但等等,这是真的吗? 最直接的验证方法是:

?- season(S), atom_length(S,L).
S = spring,
L = 6 ;
S = summer,
L = 6 ;
S = autumn,
L = 6 ;
S = winter,
L = 6.

无需findall/3,无需write/1

对于大量答案,目视检查不实用。想象400个季节。但我们可以通过以下方式验证:

?- season(S), atom_length(S,L), dif(L,6).
false.

所以我们现在肯定知道没有不同季节的季节。

这是我对你问题的第一个答案:

  

只要你可以,使用顶层外壳而不是你自己的副作用程序!进一步拉伸事物以完全避免副作用。这是从一开始就避免故障驱动循环的最佳方法。

为什么坚持顶级外壳是个好主意的原因还有很多:

  • 如果您的程序可以在顶层轻松查询,那么为它们添加测试用例将是微不足道的。

  • 顶级shell被许多其他用户使用,因此经过了很好的测试。你自己的写作往往是有缺陷的,没有经过考验。想一想约束。想想写浮标。你会使用write/1浮动吗?编写浮点数的正确方法是什么,以便可以准确地回读它们? 中执行此操作的方法。这是答案:

  

在ISO中,writeq/1,2write_canonical/1,2write_term/2,3选项quoted(true)保证可以准确回读浮点数。也就是说,它们是相同的w.r.t. (==)/2

  • toplevel shell显示有效的Prolog文本。实际上,答案本身就是一个查询!它可以粘贴回到顶层 - 只是为了得到同样的答案。通过这种方式,您将学习Prolog更具异国情调但不可避免的细节,如引用,转义和包围。实际上不可能学习语法,因为Prolog解析器通常非常宽松。

  • 您的程序很可能更容易被声明性推理所接受。

很可能,您的两个程序methodonemethodtwo不正确:您在撰写Done后忘记了换行符。所以methodone, methodone包含一个乱码。如何轻松测试?

但是让我们进一步了解您的计划。故障驱动循环的典型特征是它们无意义地开始作为“仅”副作用的东西,但迟早它们也倾向于吸引更多的语义部分。在您的情况下,atom_length/2隐藏在故障驱动循环中,完全无法通过测试或推理。

效率考虑因素

Prolog系统通常通过释放堆栈来实现失败。因此,故障驱动的循环不需要垃圾收集器。这就是为什么人们认为故障驱动的循环是有效的。但是,情况不一定如此。对于像findall(A, season(A), As)这样的目标,A的每个答案都会复制到某个空格中。这对于像原子这样的东西来说是一个微不足道的操作,但想象一个更大的术语说:

blam([]).
blam([L|L]) :- blam(L).

bigterm(L) :- length(L,64), blam(L).

在许多系统中,这个大字的findall/3assertz/1会冻结系统。

此外,像SWI,YAP,SICStus这样的系统确实拥有相当复杂的垃圾收集器。使用较少的故障驱动循环将有助于进一步改进这些系统,因为这会产生对more sophisticated techniques的需求。

答案 1 :(得分:7)

  方法一似乎很浪费,因为事实是可用的,为什么还要建立一个列表(特别是一个大的列表)。如果列表足够大,这也必须具有内存含义吗?

是的,方法1采用Θ( n )内存。它的主要好处是它是声明性的,即它具有直接的逻辑意义。

方法2,Prolog程序员称之为“故障驱动循环”,需要不间断的内存,是程序性的,当你正在进行程序性(非逻辑)事情时可能是首选;即,在I / O代码中,可以使用它。

请注意,SWI-Prolog有第三种编写此循环的方法:

forall(season(S), showseason(S)).

这仅适用于showseason的每个绑定season(S)成功的情况。

答案 2 :(得分:3)

如果已经使用findall,为什么不使用maplist

findall(S, season(S), L), maplist( showseason, L).

两者都不是纯粹的逻辑Prolog核心。是的,你为所有解决方案分配了一个完整的列表。

你的第二种方法被称为“故障驱动循环”并且它没有任何问题,除非在回溯失败之后无法获得先前的解决方案。这就是findall超逻辑的原因。在内部,它可以作为故障驱动的循环,通过断言存储其中间结果。所以第二个在概念上也更干净,除了不分配任何额外的内存。它通常用于顶级“驱动程序”(即UI)谓词。