Recursion 如何在函数程序中实现尾部递归

Recursion 如何在函数程序中实现尾部递归,recursion,functional-programming,scheme,lisp,common-lisp,Recursion,Functional Programming,Scheme,Lisp,Common Lisp,例如,以下是右折叠方案的简单实现: (define (fold-rite kons knil clist) (if (null? clist) knil (kons (car clist) (fold-rite kons knil (cdr clist))))) 这显然不是尾部调用消除的候选者,因为必须在调用kons之前完成fold-rite的递归调用。现在,我可能有点聪明,改用连续传球的方式: (define (fold-rite-cps kons knil clist

例如,以下是右折叠方案的简单实现:

(define (fold-rite kons knil clist)
  (if (null? clist)
    knil
    (kons (car clist) (fold-rite kons knil (cdr clist)))))
这显然不是尾部调用消除的候选者,因为必须在调用
kons
之前完成
fold-rite
的递归调用。现在,我可能有点聪明,改用连续传球的方式:

(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,但我对任何语言的解决方案都感兴趣;这些方法一般应适用于函数程序。

基于以上评论,我认为在不改变数据结构(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单元格的单链接性质要求遍历完整列表以到达最后一个元素,然后展开步骤或单独列表以跟踪实际折叠操作。

“如果左折叠可以在恒定空间中运行(减去累积的值)列表上的右折叠与列表背面的左折叠相同,那么我认为应该有一种方法来实现尾部递归右折叠,这种右折叠也可以在常量空间中运行。“如果结果是“在反转列表上执行左折叠”,你会失望吗?”或者它的一些变体?有点:)我想知道如果没有额外的步骤是否可能。虽然我刚想到可能不是,因为你不能在不遍历列表的情况下进行正确的折叠,不知何故,在某个时候,所以。。。我应该考虑得更多。尾部调用可以向左或向右折叠,但必须实现自己的双链表和相应的迭代器函数。或者只使用向量/数组。通常,如果您可以在不必累加的情况下进行迭代(在迭代器函数本身中,而不是在累加器函数中),并使用常量空间累加器进行累加(例如计数、最大值、最小值、总和、指向最后一个cons的指针),那么您就有了一个很好的尾部调用候选者。如果你不能(例如,迭代反向单链表,或累积中值),你仍然应该考虑不使用调用堆栈。有时你可以做到。有时,kon可以转换为关联的
append
,因此可以编写一个累积版本。当所有其他方法都失败时,就出现了CPS,可以将连续性具体化——构建为某种数据结构,而不是实际的lambda——然后在收集所有数据时进行处理(扫描整个列表)(我隐约记得Reynolds与此有关)。有时,可以在构建时简化堆上的这些数据。