【问题标题】:Fold over a partial list折叠部分列表
【发布时间】:2017-01-24 16:02:02
【问题描述】:

这是由已删除的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,在同一篇 Wikipedia 文章中引用,但这也使用了多次传递;总的来说,多个-传递算法假定对相邻像素的恒定时间访问!)。

我的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 的隐式状态,但我不得不承认我从来没有完全了解它是如何工作的;这些有联系吗?

【问题讨论】:

  • 糟糕,没注意。以为他的意思是这个问题被删除了。
  • 这是一个 SWI-Prolog 特定的问题。它假定谓词既不是标准内置谓词也不是标准库谓词,例如must_be/2foldl/4。它们甚至不是事实上的标准谓词。我会重新添加swi-prolog 标签,但喜欢假装的用户会再次删除。政治而不是事实。伤心。
  • @PauloMoura 我同意你的观点并添加了标签。很多次我看到 [swi-prolog] 标签从我什至不想费心把它放在首位的问题中删除。例如,我不知道 must_be/2foldl/4 是特定于 SWI-Prolog 的:/
  • 如何“折叠部分列表”特定于 SWI? foldl/4 绝对不是 SWI 特定的。它甚至出现在 Richard O'Keefe 的图书馆提案中。任何初学者都可以在任何 Prolog 系统中实现它。 swi-prolog 标签应该保留给明确特定于 SWI 的问题,以便用户更容易找到这些相关问题。将 SWI 提供的单个谓词在任何地方使用为“SWI”的所有内容都标记为无法找到此类实例。
  • @mat 我刚刚在阅读the same:请参阅“高阶列表谓词”。 foldl/4 就在那里。但是,must_be/2 不是。有标准吗?

标签: prolog swi-prolog fold meta-predicate


【解决方案1】:

您正在触及 Prolog 的几个非常有趣的方面,每个方面都值得几个单独的问题。我将对您的实际问题提供一个高层次的回答,并希望您就您最感兴趣的点发表后续问题。

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

本质(N):- 折叠(本质_(N),[2|返回],返回,_)。 本质_(N,X0,背部,休息):- ( X0 # X1 #= X0 + 1, 返回 = [X1|休息] ;返回 = [] )。

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

对于您的第一个问题:是的,这在 O(1) 空间中运行(假设出现整数的空间恒定)。

为什么?因为尽管您不断在 Back = [X1|Rest] 中创建列表,但这些列表都可以很容易地垃圾收集,因为您没有在任何地方引用它们。

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

?- 长度(_,E), N #= 2^E, 描绘子句(N), 精华(N), 错误的。

这会产生:

1. 2. ... 8388608。 16777216。 等等。

如果您在某处引用该列表,那将是完全不同的。例如:

本质(N):- foldl(essence_(N), [2|Back], Back, _), 返回 = []

有了这个非常小的变化,上面的查询产生:

?- 长度(_,E), N #= 2^E, 描绘子句(N), 精华(N), 错误的。 1. 2. ... 1048576。 错误:超出全局堆栈

因此,某个术语是否在某处被引用会显着影响程序的内存需求。这听起来很吓人,但实际上几乎不是问题:您要么需要该术语,在这种情况下您无论如何都需要在内存中表示它,或者您不需要该术语,在这种情况下它不再被引用在您的程序中并变得适合垃圾收集。事实上,令人惊奇的是 GC 在 Prolog 中也能很好地处理相当复杂的程序,在许多情况下不需要多说。


关于您的第二个问题:显然,使用 (-&gt;)/2 几乎总是存在很大问题,因为它会将您限制在特定的使用方向上,从而破坏了我们对逻辑关系所期望的普遍性。

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

本质_(N,X0,背部,休息):- zcompare(C, X0, N), 关闭(C,X0,返回,休息)。 关闭(

Ulrich Neumerkel 和 Stefan Kral 最近在 Indexing dif/2 中引入了另一个非常好的元谓词 if_/3。我将使用if_/3 实现这一点作为一个非常有价值和有启发性的练习。讨论这个问题值得自己提出问题


关于第三个问题:具有 DCG 的状态与此有何关系?如果您想将全局状态传递给多个谓词,DCG 表示法绝对有用,其中只有少数需要访问或修改状态,而大多数只是简单地传递状态。这完全类似于 Haskell 中的 monads

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

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

功能强连接(v) // 将 v 的深度索引设置为最小的未使用索引 v.index := 索引 v.lowlink := 索引 索引 := 索引 + 1 S.push(v)

这显然使用了全局 stackindex,它们通常会成为您需要在 all 中传递的新参数谓词。 DCG 符号并非如此!目前,假设全局实体很容易访问,因此您可以在 Prolog 中将整个片段编码为:

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

这是一个非常适合自己的问题的候选者,因此请考虑这是一个预告片。


最后,我有一个概括性的评论:在我看来,我们只是开始找到一系列非常强大和通用的元谓词,解决空间仍然很大未探索。 call/Nmaplist/[3,4]foldl/4 和其他元谓词绝对是一个好的开始。 if_/3 有可能将良好的性能与我们期望 Prolog 谓词的通用性结合起来。

【讨论】:

  • 感谢您对一个有点漫无边际的问题的彻底回答。现在,我对foldl 的奇怪使用遇到的唯一实际问题来自结束条件,从某种意义上说,我不确定 什么 它是。当foldl 的第一个参数是一个正确的列表时,结束条件是“列表为空”。在第一个示例(阶乘)和您的“本质”示例中,我们有一个额外的参数来阐明最终条件是什么。但是,在“区域”示例中,结束条件是“列表为空”......你看到那里的循环逻辑了吗?
  • 我解释了问题“是否有一种干净的方法可以在不使用剪切的情况下关闭列表”以应用于阶乘示例,它不干净,因为你是即使两个分支都可以在逻辑上应用(尝试使用更一般的查询),也不纯粹地提交条件的一个分支。诚然,它比使用!/0 更本地化,但仍然很糟糕,因为它削弱了代码的通用性。当我们在这个问题中讨论这么多问题时,我希望看到“什么是区域示例的好的元谓词”在其自己的问题中被分解,并带有指针。
【解决方案2】:

如果您的 Prolog 实现支持 freeze/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),它创建了一个从 N 到最大值 (Max) 递增的整数值的“惰性”列表,其中下一个列表元素仅在前一个元素之后生成一个被“访问”(更多内容见下文)。然后,factorial 只是将 multiplication 折叠到惰性列表上,导致内存使用量恒定。

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

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

那么,freeze定义如下:

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

这意味着如果我们调用:

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

然后会发生以下步骤:

  1. freeze(L, L=[1|Tail]) 被调用
  2. Prolog“记住”当L将与“anything”统一时,需要调用L=[1|Tail]
  3. L = [A|Rest] 被调用
  4. Prolog 将 L.(A, Rest) 统一起来
  5. 这种统一会触发 L=[1|Tail] 的执行
  6. 很明显,这将 L.(1, Tail) 结合在一起,此时它与 .(A, Rest) 绑定在一起。强>
  7. 因此,A 与 1 统一。

我们可以将这个例子扩展如下:

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 个。

【讨论】:

  • 我的问题中的第二个例子怎么样?阶乘只是原理的证明,而且选择不当。第二个例子现在存在一个真正的问题(我不知道如何知道何时关闭列表),我仍然不知道如何处理它。 freeze 有帮助吗?
【解决方案3】:

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

一般的想法是有 (1) 生成新的 Open/Closed/Rest/etc 的函数。基于前一个的状态,(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 作为第一个参数传递。

【讨论】:

  • 我需要花点时间仔细阅读和使用你的例子,因为我现在正忙于其他事情,但我会尽快让你知道。
猜你喜欢
  • 1970-01-01
  • 2019-10-29
  • 2020-10-01
  • 2020-05-26
  • 1970-01-01
  • 2018-12-24
  • 1970-01-01
  • 2023-03-20
  • 1970-01-01
相关资源
最近更新 更多