【问题标题】:What are the pros and cons of using manual list iteration vs recursion through fail使用手动列表迭代与失败递归的优缺点是什么
【发布时间】:2012-04-02 10:21:17
【问题描述】:

我总是遇到这种情况,但我永远不知道用什么方法来解决它。以下是处理某些季节事实的两种方法。

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

【问题讨论】:

    标签: prolog prolog-dif prolog-toplevel


    【解决方案1】:

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

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

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

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

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

    ?- season(S), atom_length(S,L), dif(L,6).
    错误的。
    

    所以我们现在确定没有季节长度不同。

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

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

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

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

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

    在 ISO 中,writeq/1,2write_canonical/1,2write_term/2,3 和选项 quoted(true) 保证可以准确地读回浮点数。也就是说,它们是相同的w.r.t。 (==)/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/3assertz/1 这个大术语会冻结系统。

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

    【讨论】:

      【解决方案2】:

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

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

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

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

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

      这只有在showseasonseason(S) 的每个绑定都成功时才有效。

      【讨论】:

      • 谢谢 - 我不知道 forall() - 那个逃脱了我。不错 - 现在对我很有用。
      • @larsmans:但 forall/2 本质上是一个失败驱动的循环。没有办法保留来自 forall 的绑定!那是forall(A,B)\+ \+ forall(A,B)
      • @false:是的,但不同的是 FDL 总是失败,而 forall 实际上可能会成功。实现一个循环就足够了。
      • @larsmans:forall/2 绝对比不区分成功和失败的默认失败驱动循环要好。也就是说,这个不可名状的doall(Goal) :- Goal, fail ; true. 然而,它本质上仍然对实例化极其敏感。
      【解决方案3】:

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

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

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

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

      【讨论】:

      • maplist/2,... 纯单调的(前提是它只称自己为纯单调的谓词),而findall/3不是。
      • 如果我们允许call/_,那么您说的 maplist 是纯的是正确的。但从某种意义上说,对具有特定目标的 maplist 的调用可以用纯 Prolog 编写,遵循相同的架构,是的。我不知道这里的“单调”是什么意思。
      • call/1..8 是 ISO Prolog。 W.r.t.单调的:就像在单调逻辑中一样。例如,如果我们添加更多事实,程序会发生什么?曾经真实的一切,还会继续真实吗?如果您有findall/3 或否定,则不再是这种情况。 Prolog 的核心是它的单调子集,包括call/1..8dif/2 和许多约束。它不包含 (==)/2、(\+)/1!var/1、通用 if-then-else 以及所有那些名称暂时让我无法理解的具有副作用的内置插件...
      • 为了完整性,单调还有另一个意思。简而言之,如果G 成立,那么G, ..., G 也会成立吗? var/1 没有此属性。但是nonvar/1ground/1 拥有它(除了Prolog 的所有纯部分)。
      • @magus:许多实现不再将解决方案断言到数据库中。他们宁愿将它们直接复制到堆上(a.k.a. copystack)。
      猜你喜欢
      • 2013-04-21
      • 2011-07-12
      • 2013-09-17
      • 2014-12-23
      • 1970-01-01
      • 2010-12-03
      • 1970-01-01
      • 1970-01-01
      • 2012-06-16
      相关资源
      最近更新 更多