8

我一直遇到这种情况,我永远不知道用哪种方法来攻击它。以下是处理某些季节事实的两种方法。

我正在努力解决的是使用方法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').
4

3 回答 3

10

让我们看看你的例子。它非常简单,所以我们可以想象它更复杂。但是,您似乎理所当然地认为副作用是必不可少的。让我质疑一下:

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

?-季节(S),atom_length(S,L)。
S = 弹簧,
L = 6 ;
S = 夏天,
L = 6 ;
S = 秋天,
L = 6 ;
S = 冬天,
L = 6。

不需要findall/3,不需要write/1

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

?-季节(S),atom_length(S,L),dif(L,6)。
错误的。

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

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

只要可以,使用顶层shell而不是你自己的副作用程序!将事情拉得更远一点,以完全避免副作用。这是从一开始就避免故障驱动循环的最佳方法。

坚持使用顶层 shell 是个好主意的原因还有很多:

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

  • 许多其他用户都在使用顶层 shell,因此经过了很好的测试。你自己的写作往往是有缺陷的和未经检验的。想想约束。想想写花车。你也会用write/1花车吗?编写浮点数以使其可以准确读回的正确方法是什么?中有一种方法可以做到这一点。这是答案:

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

  • 顶层 shell 向您显示有效的 Prolog 文本。事实上,答案本身就是一个查询!它可以粘贴回顶层 - 只是为了得到相同的答案。通过这种方式,您将了解 Prolog 更奇特但不可避免的细节,例如引用、转义和括号。否则几乎不可能学习语法,因为 Prolog 解析器通常非常宽容。

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

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

但是,让我们更深入地了解您的程序。失败驱动循环的典型之处在于,它们一开始是无辜的,只是做“仅”副作用的事情,但迟早它们也会吸引更多的语义部分。在您的情况下,atom_length/2隐藏在故障驱动循环中,完全无法进行测试或推理。

效率考虑

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

责备([])。
blam([L|L]):- blam(L)。

bigterm(L) :- 长度(L,64), blam(L)。

在很多系统中,findall/3或者assertz/1对于这个大术语会冻结系统。

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

于 2012-03-20T16:36:55.870 回答
7

方法一似乎很浪费,因为事实是可用的,为什么还要建立一个列表(尤其是一个大列表)。如果列表足够大,这也一定会影响内存?

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

方法 2,Prolog 程序员所称的“故障驱动循环”,占用恒定内存,是程序性的,并且无论如何在您做程序性(超逻辑)事情时可能是首选;即,在 I/O 代码中可以使用它。

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

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

这仅在.showseason的每个绑定成功时才有效season(S)

于 2012-03-16T21:23:53.927 回答
3

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

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

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

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

于 2012-03-16T22:00:58.323 回答