【问题标题】:How to achieve tail recursion in functional programs如何在函数式程序中实现尾递归
【发布时间】: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


【解决方案1】:

基于上面的 cmets,我认为在不更改数据结构(cons 单元格)的情况下,最好的答案如下(在常见的 lisp 中,因为那是我得心应手的)。

由于 cons 单元格的单链接结构,为了不累积空间,我们几乎必须切换列表的顺序,然后折叠反向列表。反转是线性空间操作,归约可能是常数空间,具体取决于所使用的归约函数。

(defun foldl (op clist &optional base)
  (if (null clist)
      base
      (foldl op (cdr clist)
             (funcall op (car clist) base))))

(defun foldr (op clist &optional base)
  ;; reverse the list then fold it
  (foldl op (foldl #'cons clist nil) base))

直接回答:不,不可能在恒定空间中折叠,因为 cons 单元格的单链接性质需要完整的列表遍历才能到达最后一个元素,然后是展开步骤或单独的列表来跟踪实际折叠操作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-25
    • 1970-01-01
    • 2019-04-07
    • 2019-11-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多