我应该避免尾递归在序言和一般?我应该避免尾递归在序言和一般?(Should I avoid tail

2019-05-12 09:22发布

我通过工作“了解现在的Prolog”为乐趣网上预定。

我试图写一个谓词,在列表中的每个成员进入,并增加了一个到它,使用蓄电池。 我已经没有尾递归容易做到了。

addone([],[]).
addone([X|Xs],[Y|Ys]) :- Y is X+1, addone(Xs,Ys).

但我已阅读,最好是避免这种类型的递归性能方面的原因。 这是真的? 是它认为“良好做法”用尾递归始终? 这将是值得用蓄电池进入一个好习惯的努力?

我曾试图改变这个例子中使用到蓄电池,但反转的列表。 我怎样才能避免这种情况?

accAddOne([X|Xs],Acc,Result) :- Xnew is X+1, accAddOne(Xs,[Xnew|Acc],Result).
accAddOne([],A,A).
addone(List,Result) :- accAddOne(List,[],Result).

Answer 1:

简短的回答:尾递归是可取的,但不要过分强调它。

你原来的程序是尾递归,你可以在Prolog的获得。 但也有更重要的问题:正确性和终止。

事实上,许多实现比愿意牺牲尾递归,因为他们认为更重要的其他属性的更多。 例如坚定性 。

但是,你尝试优化具有一定的点。 在从历史的角度最小。

早在上世纪70年代,主要的AI语言是LISP。 而相应的定义本来

(defun addone (xs)
  (cond ((null xs) nil)
    (t (cons (+ 1 (car xs))
         (addone (cdr xs))))))

这是不直接尾递归:原因是cons :在该时间实现方式中,它的参数进行了评价第一,只有这时, cons可以被执行。 因此重写这是你已经指出(和扭转的结果列表)是一个可能的优化技术。

在Prolog,但是,您可以创建之前知道实际值的缺点,由于逻辑变量。 这么多,不是尾递归在Lisp程序,转化为在序言尾递归程序。

这样做的后果仍然可以在许多Prolog的教科书中找到。



Answer 2:

你addOne程序已经尾递归的。

有头部和最后的递归调用之间没有选择点,因为是/ 2是确定性的。

累加器有时添加到允许尾递归,简单的例子中,我能想到的是扭转/ 2。 这里是一个幼稚反向(nreverse / 2),非尾递归

nreverse([], []).
nreverse([X|Xs], R) :- nreverse(Xs, Rs), append(Rs, [X], R).

如果我们增加一个累加器

reverse(L, R) :- reverse(L, [], R).
reverse([], R, R).
reverse([X|Xs], A, R) :- reverse(Xs, [X|A], R).

现在扭转/ 3是尾递归:递归调用是最后一个,并且没有选择点是左。



Answer 3:

OP说:

但我已阅读,这是更好地避免[尾巴]递归出于性能的考虑。 这是真的? 是它认为“良好做法”用尾递归始终? 这将是值得用蓄电池进入一个好习惯的努力?

这是一个相当简单的优化转换尾递归构造成迭代(循环)。 由于尾(递归)调用的最后一件事做,堆栈帧可以在递归调用简单地跳转到谓词/功能/方法开始重用,使递归,对于所有意图和目的,一个循环, /子程序。 因此,尾递归谓词不会溢出堆栈。 尾递归结构,应用了具有以下优点优化:

  • 稍快执行新的堆栈帧不需要分配/释放; 另外,您得到更好的参考局部性,所以可以说是少分页。
  • 没有上界递归的深度。
  • 没有堆栈溢出。

可能的缺点?

  • 有用的堆栈跟踪的损失。 若禁制令以释放/优化的建立仅适用,而不是在调试版本,但一个问题...
  • 开发人员编写依赖于TRO,这意味着代码将运行罚款与应用,无需应用TRO将失败TRO的代码。 这意味着在上述情况下(只在释放TRO /优化版本),释放和调试版本之间存在功能性变化,基本上意一个的选择的编译选项生成从相同的源代码的两个不同的方案。

这不,当然,一个问题,当语言标准要求尾递归优化。

引述维基百科:

尾调用是显著,因为它们可以在不增加新的堆栈帧调用栈来实现。 大多数当前过程的框架的不需要任何更多的,并且它可以通过尾呼叫的帧来代替,适当变更(类似于用于覆盖处理,但对于函数调用)。 然后该程序可以跳转到被调用的子程序。 生产这样的代码,而不是标准的呼叫序列称为尾调用消除,或尾部调用优化。

也可以看看:

  • 什么是尾调用优化?
  • 对此事波特兰模式知识库
  • 甚至微软的MSDN

我永远无法理解为什么越来越多的语言不实行尾递归优化



Answer 4:

我不认为第一个版本addone应导致代码效率不高。 这也是一个很大的可读性,所以我不明白为什么它应该是很好的做法,以避免它。

在更复杂的例子,编译器可能无法自动传输代码尾递归。 然后,它可以合理地把它改写为优化,但只有当它是真的有必要。

那么,你怎么能实现的工作尾递归版本addone ? 这可能是在欺骗,但假设reverse是与尾递归实现(例如,见这里 ),那么它可以用来解决您的问题:

accAddOne([X|Xs],Acc,Result) :- Xnew is X+1, accAddOne(Xs,[Xnew|Acc],Result).
accAddOne([],Acc,Result) :- reverse(Acc, Result).
addone(List,Result) :- accAddOne(List,[],Result).

它的极端笨拙,虽然。 :-)

顺便说一句,我无法找到一个简单的解决方案。 这可能是因为相同的理由, foldr Haskell中通常不与尾递归定义。



文章来源: Should I avoid tail recursion in Prolog and in general?