这是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
那样明确结束条件,所以我需要在它之后进行切割(丑陋)。
答案 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/N
,maplist/[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].
然后会发生以下步骤:
我们可以按如下方式扩展这个例子:
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作为第一个参数传递。