我一直反对这一点,我无法确定攻击它的方法。以下是处理某些季节事实的两种方法。
我想弄清楚的是,是否使用方法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').
答案 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-prolog中执行此操作的方法。这是答案:
在ISO中,
writeq/1,2
,write_canonical/1,2
,write_term/2,3
选项quoted(true)
保证可以准确回读浮点数。也就是说,它们是相同的w.r.t.(==)/2
toplevel shell显示有效的Prolog文本。实际上,答案本身就是一个查询!它可以粘贴回到顶层 - 只是为了得到同样的答案。通过这种方式,您将学习Prolog更具异国情调但不可避免的细节,如引用,转义和包围。实际上不可能学习语法,因为Prolog解析器通常非常宽松。
您的程序很可能更容易被声明性推理所接受。
很可能,您的两个程序methodone
和methodtwo
不正确:您在撰写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/3
或assertz/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)谓词。