【问题标题】:Is the `append` predicate tail-recursive?`append` 谓词是尾递归的吗?
【发布时间】:2013-07-05 13:02:22
【问题描述】:

Prolog中列表处理的典型代码示例为append

append([], Ys, Ys).
append([X | Xs], Ys, [X | Zs]) :- append(Xs, Ys, Zs).

我的问题是这个程序是否是尾递归的。我猜不是根据我在函数式语言方面的经验。但是,我发现 Prolog 程序更难判断。看来我们必须考虑统一。

【问题讨论】:

标签: prolog logic-programming


【解决方案1】:

是的,您的append/3(以及因此的 Prolog“标准”版本)是尾递归的。您可以很容易地看到这一点,因为最终目标是调用 append/3 本身。请注意,函数式语言中append 的典型实现不是尾递归的,因为最终调用是等效于Lisp 中cons 的操作,例如对应于:

lisp_append([], Ys, Ys).
lisp_append([X|Xs], Ys, Zs) :-
        lisp_append(Xs, Ys, Zs0),
        Zs = [X|Zs0].

示例查询,由于无法应用尾调用优化,导致 本地 堆栈溢出:

?- length(Ls, 10_000_000), lisp_append(Ls, [], _).
ERROR: Out of local stack

append/3 的自然 Prolog 版本有效:

?- length(Ls, 10_000_000), append(Ls, [], _).
Ls = [_G8, _G11, _G14, _G17, _G20, _G23, _G26, _G29, _G32|...].

请注意,与函数式语言相比,Prolog 中更多的谓词自然是尾递归的,这是由于统一的力量可以让您在尾调用之前提取部分结果的描述。 +1 提出一个好问题。

【讨论】:

  • 感谢您的出色回答。但是恐怕我没有完全理解“拉……”的意思。请您详细说明一下?
  • 例如lisp_append/3可以交换最后两个目标,得到一个尾递归谓词。更进一步,您甚至可以将统一拉到子句头部,这是在 Prolog 中最自然的编写方式,也是您从一开始就编写的。您无法在函数式语言中交换这些调用的顺序,因为它们无法对部分实例化的结果进行推理,例如列表 [X|Zs0],其中 Zs0 尚不可知。函数式语言的一个相当恼人的特性是,像append 这样的基本函数不是自然地尾递归的。
【解决方案2】:

不,您的代码不是尾递归的。 尾递归意味着在递归的底部,您可以直接得到您在开始时要求的答案。

如果你跟踪你的代码,例如append([1,2,3],[a,b,c],Out),你会得到:

Call:append([1, 2, 3], [a, b, c], _G4702)
Call:append([2, 3], [a, b, c], _G4756)
Call:append([3], [a, b, c], _G4759)
Call:append([], [a, b, c], _G4762)
Exit:append([], [a, b, c], [a, b, c])
Exit:append([3], [a, b, c], [3, a, b, c])
Exit:append([2, 3], [a, b, c], [2, 3, a, b, c])
Exit:append([1, 2, 3], [a, b, c], [1, 2, 3, a, b, c])

变量(_G4762,_G4759,_G4756)的值被传递给_G4702_G4702就是答案。

我们可能有一个尾递归版本的追加:

ap_tail_r([H|T],B,Ac,Out):-
   ap_tail_r(T,B,[H|Ac],Out).

ap_tail_r([],B,[H|Ac],Out):-
   ap_tail_r([],[H|B],Ac,Out).

ap_tail_r([],Out,[],Out).

让我们再次追踪ap_tail_r([1,2,3],[a,b,c],[],Out)

Call:ap_tail_r([1, 2, 3], [a, b, c], [], _G4786)
Call:ap_tail_r([2, 3], [a, b, c], [1], _G4786)
Call:ap_tail_r([3], [a, b, c], [2, 1], _G4786)
Call:ap_tail_r([], [a, b, c], [3, 2, 1], _G4786)
Call:ap_tail_r([], [3, a, b, c], [2, 1], _G4786)
Call:ap_tail_r([], [2, 3, a, b, c], [1], _G4786)
Call:ap_tail_r([], [1, 2, 3, a, b, c], [], _G4786)
Exit:ap_tail_r([], [1, 2, 3, a, b, c], [], [1, 2, 3, a, b, c])
Exit:ap_tail_r([], [2, 3, a, b, c], [1], [1, 2, 3, a, b, c])
Exit:ap_tail_r([], [3, a, b, c], [2, 1], [1, 2, 3, a, b, c])
Exit:ap_tail_r([], [a, b, c], [3, 2, 1], [1, 2, 3, a, b, c])
Exit:ap_tail_r([3], [a, b, c], [2, 1], [1, 2, 3, a, b, c])
Exit:ap_tail_r([2, 3], [a, b, c], [1], [1, 2, 3, a, b, c])
Exit:ap_tail_r([1, 2, 3], [a, b, c], [], [1, 2, 3, a, b, c])

我们保留的唯一变量是_G4786,这是我们首先要寻找的答案。

尾递归代码的确切作用是:反转第一部分,b。将颠倒的第一部分一头接一头地放在第二部分,c。当保留的第一部分为空时,更新的第二部分为附加结果。

【讨论】:

  • 什么?!你的回答有问题。有关详细信息,请查看@mat 的上述答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-10-09
  • 1970-01-01
  • 2021-12-12
  • 1970-01-01
  • 1970-01-01
  • 2011-04-16
  • 1970-01-01
相关资源
最近更新 更多