折叠部分列表

时间:2016-09-16 12:12:25

标签: prolog swi-prolog fold meta-predicate

这是this question已删除答案引发的问题。问题可归纳如下:

  

是否可以折叠列表,折叠时生成列表的尾部?

这就是我的意思。假设我想计算阶乘(这是一个愚蠢的例子,但它仅用于演示),并决定这样做:

fac_a(N, F) :-
        must_be(nonneg, N),
        (       N =< 1
        ->      F = 1
        ;       numlist(2, N, [H|T]),
                foldl(multiplication, T, H, F)
        ).

multiplication(X, Y, Z) :-
        Z is Y * X.

在这里,我需要生成我给foldl的列表。但是,我可以在常量内存中执行相同的操作(不生成列表而不使用foldl):

fac_b(N, F) :-
        must_be(nonneg, N),
        (       N =< 1
        ->      F = 1
        ;       fac_b_1(2, N, 2, F)
        ).

fac_b_1(X, N, Acc, F) :-
        (       X < N
        ->      succ(X, X1),
                Acc1 is X1 * Acc,
                fac_b_1(X1, N, Acc1, F)
        ;       Acc = F
        ).

这里的要点是,与使用foldl的解决方案不同,它使用常量内存:无需生成包含所有值的列表!

计算阶乘并不是最好的例子,但接下来的愚蠢更容易理解。

假设我真的害怕循环(和递归),并且坚持使用折叠来计算阶乘。不过,我仍然需要一份清单。所以我可以尝试这样做:

fac_c(N, F) :-
        must_be(nonneg, N),
        (       N =< 1
        ->      F = 1
        ;       foldl(fac_foldl(N), [2|Back], 2-Back, F-[])
        ).

fac_foldl(N, X, Acc-Back, F-Rest) :-
        (       X < N
        ->      succ(X, X1),
                F is Acc * X1,
                Back = [X1|Rest]
        ;       Acc = F,
                Back = []
        ).

令我惊讶的是,这是按预期工作的。我可以在部分列表的头部使用初始值“播种”折叠,并在消耗当前头部时继续添加下一个元素。 fac_foldl/4的定义几乎与上面fac_b_1/4的定义相同:唯一的区别是状态维持不同。我的假设是这应该使用恒定的记忆:假设是错误的吗?

我知道这很愚蠢,但它可以用于折叠折叠开始时无法知道的列表。在原始问题中,我们必须找到一个连通区域,给出一个x-y坐标列表。折叠xy坐标列表一次是不够的(你可以do it in two passes;注意至少有one better way to do it,在同一篇维基百科文章中引用,但这也使用了多次传递;完全相同,多次通过算法假设对相邻像素进行恒定时间访问!)。

我的own solution to the original "regions" question看起来像这样:

set_region_rest([A|As], Region, Rest) :-
        sort([A|As], [B|Bs]),
        open_set_closed_rest([B], Bs, Region0, Rest),
        sort(Region0, Region).

open_set_closed_rest([], Rest, [], Rest).
open_set_closed_rest([X-Y|As], Set, [X-Y|Closed0], Rest) :-
        X0 is X-1, X1 is X + 1,
        Y0 is Y-1, Y1 is Y + 1,
        ord_intersection([X0-Y,X-Y0,X-Y1,X1-Y], Set, New, Set0),
        append(New, As, Open),
        open_set_closed_rest(Open, Set0, Closed0, Rest).

使用与上面相同的“技术”,我们可以将其扭曲成折叠:

set_region_rest_foldl([A|As], Region, Rest) :-
        sort([A|As], [B|Bs]),
        foldl(region_foldl, [B|Back],
                            closed_rest(Region0, Bs)-Back,
                            closed_rest([], Rest)-[]),
        !,
        sort(Region0, Region).

region_foldl(X-Y,
             closed_rest([X-Y|Closed0], Set)-Back,
             closed_rest(Closed0, Set0)-Back0) :-
        X0 is X-1, X1 is X + 1,
        Y0 is Y-1, Y1 is Y + 1,
        ord_intersection([X0-Y,X-Y0,X-Y1,X1-Y], Set, New, Set0),
        append(New, Back0, Back).

这也“有效”。折叠留下了一个选择点,因为我没有像上面的fac_foldl/4那样明确结束条件,所以我需要在它之后进行切割(丑陋)。

问题

  • 是否有一种干净的方法来关闭列表并删除剪切?在阶乘示例中,我们知道何时停止,因为我们有其他信息;但是,在第二个例子中,我们如何注意到列表的后面应该是空列表?
  • 我遗失了一个隐藏的问题吗?
  • 这看起来有点类似于DCG的隐含状态,但我不得不承认我从来没有完全了解它是如何工作的;这些是连接的吗?

3 个答案:

答案 0 :(得分:2)

你正在触及Prolog的几个非常有趣的方面,每个方面都值得几个单独的问题。我将为您的实际问题提供高级答案,并希望您就最感兴趣的问题发布后续问题。

首先,我将把片段修剪为其本质:

essence(N) :-
        foldl(essence_(N), [2|Back], Back, _).

essence_(N, X0, Back, Rest) :-
        (   X0 #< N ->
            X1 #= X0 + 1,
            Back = [X1|Rest]
        ;   Back = []
        ).

请注意,这可以防止创建极大的整数,这样我们就可以真正研究这种模式的内存行为。

关于你的第一个问题:,它在 O(1)空间中运行(假设产生整数的空间不变)。

为什么?因为虽然您在Back = [X1|Rest]中不断创建列表,但这些列表都可以随时垃圾回收,因为您没有在任何地方引用它们。

要测试程序的内存方面,请考虑以下查询,并限制Prolog系统的全局堆栈,以便通过耗尽(全局)堆栈快速检测增长的内存:

?- length(_, E),
   N #= 2^E,
   portray_clause(N),
   essence(N),
   false.

这会产生:

1.
2.
...
8388608.
16777216.
etc.

如果在某个地方引用列表,完全会有所不同。例如:

essence(N) :-
        foldl(essence_(N), [2|Back], Back, _),
        Back = [].

通过这个非常小的更改,上面的查询产生:

?- length(_, E),
   N #= 2^E,
   portray_clause(N),
   essence(N),
   false.
1.
2.
...
1048576.
ERROR: Out of global stack

因此,某个术语是否被引用可能会显着影响程序的内存需求。这听起来非常可怕,但在实践中确实不是一个问题:你要么需要这个术语,在这种情况下你需要在内存中代表它,或者你不需要这个术语,在这种情况下它就不再被引用了在你的程序中,变得适合垃圾收集。事实上,令人惊讶的是,GC在Prolog中也能很好地运行,对于非常复杂的程序而言,在很多情况下都不需要说太多。

关于你的第二个问题:很明显,使用(->)/2几乎总是存在很大问题,因为它限制了你使用的特定方向,破坏了我们对逻辑关系的期望。

有几种解决方案。如果您的CLP(FD)系统支持zcompare/3或类似功能,您可以按如下方式编写essence_/3

essence_(N, X0, Back, Rest) :-
        zcompare(C, X0, N),
        closing(C, X0, Back, Rest).

closing(<, X0, [X1|Rest], Rest) :- X1 #= X0 + 1.
closing(=, _, [], _).

另一个非常好的元谓词叫做 if_/3 ,最近由Ulrich Neumerkel和Stefan Kral在Indexing dif/2中引入。我将if_/3作为一项非常有价值和有益的练习。讨论这个值得自己提出问题

关于第三个问题:与DCG有关的国家如何与此相关?如果要将全局状态传递给多个谓词,DCG表示法绝对有用,其中只有少数谓词需要访问或修改状态,并且大多数谓词只是通过状态。这完全类似于Haskell中的 monads

“普通”Prolog解决方案是使用2个参数扩展每个谓词,以描述谓词调用之前的状态与其后的状态之间的关系。 DCG表示法可以避免这种麻烦。

重要的是,使用DCG表示法,您可以将命令式算法几乎逐字复制到Prolog,即使您需要全局状态,也无需引入许多辅助参数的麻烦。作为一个例子,请用命令性的术语来考虑Tarjan strongly connected components算法的一个片段:

  function strongconnect(v)
    // Set the depth index for v to the smallest unused index
    v.index := index
    v.lowlink := index
    index := index + 1
    S.push(v)

这显然使用了全局堆栈索引,这通常会成为您需要在所有中传递的新参数谓词。 DCG表示法不是这样!目前,假设全局实体只是易于访问,因此您可以将整个片段编码为Prolog:

scc_(V) -->
        vindex_is_index(V),
        vlowlink_is_index(V),
        index_plus_one,
        s_push(V),

这是一个非常适合自己的问题的候选人,所以请考虑这是一个预告片。

最后,我有一个一般性的评论:在我看来,我们只是在开始找到一系列非常强大和一般的元谓词,解决方案空间仍然很大程度上< EM>未知。 call/Nmaplist/[3,4]foldl/4和其他元谓词绝对是一个好的开始。 if_/3有可能将良好的表现与我们对Prolog谓词的期望结合起来。

答案 1 :(得分:0)

如果您的Prolog实现支持冻结/ 2 或类似的谓词(例如Swi-Prolog),那么您可以使用以下方法:

fac_list(L, N, Max) :-
    (N >= Max, L = [Max], !)
    ;
    freeze(L, (
        L = [N|Rest],
        N2 is N + 1,
        fac_list(Rest, N2, Max)
    )).

multiplication(X, Y, Z) :-
    Z is Y * X.

factorial(N, Factorial) :-
    fac_list(L, 1, N),
    foldl(multiplication, L, 1, Factorial).

上面的示例首先定义了一个谓词( fac_list ),它创建了一个&#34; lazy&#34;从N开始到最大值(Max)的递增整数值的列表,其中下一个列表元素仅在前一个列表元素被访问之后生成#34; (更多内容见下文)。然后, factorial 会在惰性列表中折叠乘法,从而导致内存使用量不断增加。

理解这个例子如何运作的关键在于记住,Prolog列表实际上只是名称&#39;的arity 2的术语。 (实际上,在Swi-Prolog 7中,名称已更改,但这对于此讨论并不重要),其中第一个元素表示列表项,第二个元素表示尾(或终止元素 - 空列表,[])。例如。 [1,2,3]可以表示为:

.(1, .(2, .(3, [])))

然后,冻结定义如下:

freeze(+Var, :Goal)
    Delay the execution of Goal until Var is bound

这意味着如果我们打电话:

freeze(L, L=[1|Tail]), L = [A|Rest].

然后会发生以下步骤:

  1. 冻结(L,L = [1 | Tail])被称为
  2. Prolog&#34;记得&#34;当 L 与&#34;任何&#34;统一时,需要调用 L = [1 | Tail]
  3. L = [A | Rest] 被称为
  4. Prolog将 L 统一起来。(A,休息)
  5. 此统一会触发执行 L = [1 | Tail]
  6. 这显然统一了 L ,此时与。(A,休息)绑定,。(1,Tail)
  7. 因此, A 与1统一。
  8. 我们可以按如下方式扩展这个例子:

    freeze(L1, L1=[1|L2]),
    freeze(L2, L2=[2|L3]),
    freeze(L3, L3=[3]),
    L1 = [A|R2], % L1=[1|L2] is called at this point
    R2 = [B|R3], % L2=[2|L3] is called at this point
    R3 = [C].    % L3=[3] is called at this point
    

    这与上一个示例完全相同,只是它逐渐生成3个元素,而不是1个。

答案 2 :(得分:0)

根据Boris的要求,第二个示例使用冻结实施。老实说,我不太确定这是否能回答这个问题,因为代码(和IMO,问题)是相当人为的,但现在就是这样。至少我希望这会让其他人知道冻结可能有用的东西。为简单起见,我使用1D问题而不是2D,但更改代码以使用2坐标应该相当简单。

一般的想法是拥有(1)生成新的打开/关闭/休息/等的功能。状态基于前一个,(2)“无限”列表生成器,可以被告知“停止”从“外部”生成新元素,以及(3)fold_step函数折叠“无限”列表,在每个上生成新状态列表项,如果该状态被认为是最后一个,则告知生成器停止。

值得注意的是,列表的元素没有其他原因用于通知生成器停止。所有计算状态都存储在累加器中。

Boris ,请说明这是否可以解决您的问题。更确切地说,您试图传递给折叠步骤处理程序(Item,Accumulator,Next Accumulator)的数据类型是什么?

adjacent(X, Y) :-
    succ(X, Y) ;
    succ(Y, X).

state_seq(State, L) :-
    (State == halt -> L = [], !)
    ;
    freeze(L, (
        L = [H|T],
        freeze(H, state_seq(H, T))
    )).

fold_step(Item, Acc, NewAcc) :-
    next_state(Acc, NewAcc),
    NewAcc = _:_:_:NewRest,
    (var(NewRest) ->
        Item = next ;
        Item = halt
    ).

next_state(Open:Set:Region:_Rest, NewOpen:NewSet:NewRegion:NewRest) :-
    Open = [],
    NewOpen = Open,
    NewSet = Set,
    NewRegion = Region,
    NewRest = Set.

next_state(Open:Set:Region:Rest, NewOpen:NewSet:NewRegion:NewRest) :-
    Open = [H|T],
    partition(adjacent(H), Set, Adjacent, NotAdjacent),
    append(Adjacent, T, NewOpen),
    NewSet = NotAdjacent,
    NewRegion = [H|Region],
    NewRest = Rest.

set_region_rest(Ns, Region, Rest) :-
    Ns = [H|T],
    state_seq(next, L),
    foldl(fold_step, L, [H]:T:[]:_, _:_:Region:Rest).

上面代码的一个很好的改进是使fold_step成为一个更高阶的函数,将next_state作为第一个参数传递。