重写这种非尾递归函数有什么好方法?

时间:2008-11-24 21:19:10

标签: language-agnostic functional-programming recursion tree clojure

出于某种原因,我无法想出一种重写此函数的好方法,因此它使用了不变的堆栈空间。大多数关于树递归的在线讨论都是通过使用Fibonacci函数并利用该特定问题的属性来作弊。有没有人对这个“真实世界”(嗯,比Fibonacci系列更现实世界)使用递归有任何想法?

Clojure是一个有趣的案例,因为它没有尾调用优化,只有尾递归通过“recur”特殊形式。它也强烈反对使用可变状态。它确实有许多惰性结构,包括tree-seq,但我无法看到他们如何帮助我这个案例。任何人都可以分享他们从C,Scheme,Haskell或其他编程语言中学到的一些技巧吗?

(defn flatten [x]
  (let [type (:type x)]
    (cond (or (= type :NIL) (= type :TEXT))
          x
          (= type :CONCAT)
          (doc-concat (flatten (:doc1 x))
                      (flatten (:doc2 x)))
          (= type :NEST)
          (doc-nest (:level x)
                    (flatten (:doc x)))
          (= type :LINE)
          (doc-text " ")
          (= type :UNION)
          (recur (:doc1 x)))))

编辑:通过评论中的请求...

以一般术语和使用Scheme重述 - 如何重写以下递归模式,以便它不消耗堆栈空间或需要非自调用的尾调用优化?

(define (frob x)
  (cond ((foo? x)
         x)
        ((bar? x)
         (macerate (f x) (frob (g x))))
        ((thud? x)
         (frobnicate (frob (g x))
                     (frob (h x))))))

我选择恼人的名字来回家,我正在寻找不依赖于x,macerate,frobnicate,f,g或h的代数属性的答案。我只想重写递归。

更新

Rich Hickey已经向Clojure添加了一个明确的trampoline function

7 个答案:

答案 0 :(得分:6)

请不要贬低这一点,因为它太丑了。我知道这很难看。但这是一种在trampoline风格(没有系统堆栈溢出),并且不使用gotos的方式。

push x,1 on homemade stack
while stack length > 1
  n = pop
  if (n==1)
    x = pop
    if (type(x)==NIL || type(x)==TEXT)
      push x // this is the "return value"
    else if (type(x)==CONCAT)
      push 2 // say call doc-concat
      push doc2(x), 1 // 2nd recursion
      push doc1(x), 1 // 1st recursion
    else if (type(x)==NEST)
      push 3 // say call doc-nest
      push level(x) // push level argument to doc-nest
      push doc(x), 1 // schedule recursion
    else if (type(x)==LINE)
      push " " // return a blank
    else if (type(x)==UNION)
      push doc1(x), 1 // just recur
  else if (n==2)
    push doc-concat(pop, pop) // finish the CONCAT case
  else if (n==3)
    push doc-nest(pop, pop) // finish the NEST case
  endif
endwhile
// final value is the only value on the stack

答案 1 :(得分:3)

轻松转换算法的主要障碍是它不会导致对同一函数的一系列调用;但是在几个之间交替,每个都根据另一个的结果进行操作。

我说你有三种选择:

  1. 完全重新制定算法(这就是Fibonacci的例子)。
    • 将所有函数组合成一个包含大量cond的函数(丑陋,即使使用单个函数也可能不会产生真正的尾递归)。
    • 将流程从里向外翻转:编写一个简单的尾递归函数,将输入数据转换为必须执行的操作序列,然后对其进行评估。

答案 2 :(得分:2)

如果flatten自己调用两次(在:CONCAT情况下)它怎么能变成循环?也许我错过了什么。看起来它本身就是一个树木漫步。

我的意思是,有一些方法可以在没有堆栈的情况下进行树木漫游,但是必须有一些无限制的东西,比如你是用FIFO做的,或者是建议的,有连续性的。

答案 3 :(得分:2)

你可以使用延续传递:

(define (frob0 x k)
  (cond ((foo? x)
         (k x))
        ((bar? x)
         (frob0 (g x) 
           (lambda (y) 
             (k (macerate (f x) y))))
        ((thud? x)
         (frob0 (g x) 
           (lambda (y)
             (frob0 (h x) 
               (lambda (z)
                 (k (frobnicate y z))))))))

(define (frob x)
  (frob0 x (lambda (y) y))

这不会让事情变得更容易理解: - (

答案 4 :(得分:2)

标准通用技术是转换为trampolined style。对于您的特定问题(实现漂亮印刷组合器),您可能会发现有用的Derek Oppen 1980年论文“Prettyprinting”(不在网上AFAIK上)。它提出了一种基于堆栈的命令式算法,类似于Wadler后来的功能性算法。

答案 5 :(得分:0)

我能想到的最好的是:

(define (doaction vars action)
  (cond ((symbol=? action 'frob)
         (cond ((foo? (first vars))
                (first vars))
               ((bar? (first vars))
                (doaction (list (f (first vars)) (doaction (g x) 'frob)) 'macerate)
etc...

它不是完全尾递归,但可能是最好的。 TCO真的是要走的路。 (据我所知,由于JVM,Clojure无法拥有它)。

答案 6 :(得分:0)

以下内容并非您问题的具体答案,但希望这将是一个有用的示例。它用一堆任务替换了多个递归(否则需要一个无限制的调用堆栈)。

(在Haskellish代码中):

data Tree = Null | Node Tree Val Tree

-- original, non-tail-recursive function: flatten :: Tree -> Result flatten Null = nullval flatten (Node a v b) = nodefunc (flatten a) v (flatten b)

-- modified, tail-recursive code: data Task = A Val Tree | B Result Val

eval :: Tree -> [Task] -> Result use :: Result -> [Task] -> Result

eval Null tasks = use nullval tasks eval (Node a v b) tasks = eval a ((A v b):tasks)

use aval ((A v b):tasks) = eval b ((B aval v):tasks) use bval ((B aval v):tasks) = use (nodefunc aval v bval) tasks use val [] = val

-- actual substitute function flatten2 :: Tree -> Result flatten2 tree = eval tree []