【发布时间】:2015-03-11 10:17:52
【问题描述】:
以 Scheme 中右折叠的以下简单实现为例:
(define (fold-rite kons knil clist)
(if (null? clist)
knil
(kons (car clist) (fold-rite kons knil (cdr clist)))))
这显然不是尾调用消除的候选者,因为fold-rite 的递归调用必须在调用kons 之前完成。现在,我可能有点聪明,改用延续传递风格:
(define (fold-rite-cps kons knil clist kontinuation)
(if (null? clist)
(kontinuation knil)
(fold-rite-cps kons knil (cdr clist)
(lambda (x) (kontinuation (kons (car clist) x))))))
现在fold-rite-cps 是尾递归的,但是在递归期间建立的中间延续仍然必须保留在某个地方。所以虽然我可能不会炸毁堆栈,但我仍然使用与第一个版本一样多的内存,不知何故,我觉得首先收集大量的延续,然后一举解开它们应该是对性能不利,尽管第二个版本在我的机器上实际上要快得多。
但是如果左折叠可以在恒定空间中运行(减去正在累积的值)并且列表上的右折叠与其相反的左折叠相同,那么我想应该有一种方法来实现尾部-递归右折叠,也可以在恒定空间中运行。如果是这样,我该怎么做?更笼统地说,有什么方法可以将非尾递归函数变成尾递归函数,最好是可以在常量空间中运行的函数(假设显式循环可以用命令式语言编写,该语言也可以在常量中运行空间)?我做了什么错误的假设吗?
虽然我标记了 Scheme 和 Lisp,但我对任何语言的解决方案都很感兴趣;这些方法应该适用于一般的功能程序。
【问题讨论】:
-
"如果左折叠可以在恒定空间中运行(减去正在累积的值)并且列表上的右折叠与其反面的左折叠相同,那么我想应该有一个实现尾递归右折叠的方法,它也可以在恒定空间中运行。”如果结果是“在反向列表上做左折叠”或它的某种变体,你会失望吗?
-
一点 :) 我想知道如果没有那个额外的步骤是否有可能。虽然我突然想到这可能不是因为如果不遍历列表就无法正确折叠,但不知何故,在某个时候,所以......我应该更多地考虑这一点。
-
尾调用可以向左或向右折叠,但您必须实现自己的双向链表和相应的迭代器函数。或者只使用向量/数组。一般来说,如果你可以迭代而不必累加(在迭代器函数本身,而不是在累加器函数中),并用一个常数空间累加器进行累加(例如计数、最大值、最小值、总和、指向最后一个缺点的指针),你有一个好的尾随候选人。如果您不能(例如,迭代反向单链表或累积中位数),您仍然应该考虑不使用调用堆栈。
-
有时您可以使用en.wikipedia.org/wiki/Tail_call#Tail_recursion_modulo_cons。有时 kons 可以变成关联的
append,因此可以编写一个累积版本。当所有其他方法都失败时,有 CPS,并且延续可以被具体化 - 构建为某种数据结构,而不是实际的 lambdas - 然后在收集所有数据时进行处理(扫描整个列表)(我隐约记得 Reynolds'en.wikipedia.org/wiki/Defunctionalization与此有关)。有时,堆上的这些数据可以在构建时进行简化。
标签: recursion functional-programming scheme lisp common-lisp