如何才能使此函数成为尾递归?

时间:2016-11-09 15:23:21

标签: recursion f# stack-overflow tail-recursion continuations

still试图实施2-3个手指树,我取得了很好的进展(repository)。在做一些基准测试时,我发现当树很大时,我的基本toList导致StackOverflowException。起初我看到一个简单的修复,并使其尾递归。

不幸的是,事实证明toList不是罪魁祸首,但viewr是:

/// Return both the right-most element and the remaining tree (lazily).
let rec viewr<'a> : FingerTree<'a> -> View<'a> = function
    | Empty -> Nil
    | Single x -> View(x, lazyval Empty)
    | Deep(prefix, deeper, One x) ->
        let rest = lazy (
            match viewr deeper.Value with
            | Nil ->
                prefix |> Digit.promote
            | View (node, lazyRest) ->
                let suffix = node |> Node.toList |> Digit.ofList
                Deep(prefix, lazyRest, suffix)
        )
        View(x, rest)
    | Deep(prefix, deeper, Digit.SplitLast(shorter, x)) ->
        View(x, lazy Deep(prefix, deeper, shorter))
    | _ -> failwith Messages.patternMatchImpossible

寻找唯一的递归调用很明显,这不是尾递归。不知何故,我希望这个问题不会存在,因为该调用包含在Lazy中,其中恕我直言类似于延续。

我听说并读到延续,但到目前为止从未(不得不)使用(d)它们。我想这里我真的需要。我已经盯着代码很长一段时间了,把函数参数放在那里,把它们称为其他地方...... 我完全失去了!

如何做到这一点?

更新:调用代码如下所示:

/// Convert a tree to a list (left to right).
let toList tree =
    let rec toList acc tree =
        match viewr tree with
        | Nil -> acc
        | View(head, Lazy tail) -> tail |> toList (head::acc)
    toList [] tree

更新2:导致崩溃的代码就是这个。

let tree = seq {1..200000} |> ConcatDeque.ofSeq
let back = tree |> ConcatDeque.toList

树变得很好,我检查了它只有12级深。第2行中的调用触发了溢出。

更新3: kvb是对的,pipe issue我之前遇到过这个问题。重新测试调试/发布和使用/不使用管道的交叉产品除了一种情况外它在所有情况下工作:管道运算符崩溃的调试模式。 32对64位的行为相同。

我很确定在发布问题时我正在运行发布模式,但今天它正在运行。也许还有其他一些因素......很抱歉。

虽然崩溃已经解决,但我仍然没有理论上的兴趣。毕竟,我们来这里学习,不是吗?

让我改编一下这个问题:
从查看代码来看,viewr绝对不是尾递归的。为什么它总是不会爆炸?如何使用延续重写它呢?

1 个答案:

答案 0 :(得分:2)

调用viewr永远不会导致对viewr的立即递归调用(递归调用受lazy保护,并且不会强制调用viewr的剩余调用),所以没有必要使它的尾递归,以防止堆栈无限制地增长。也就是说,对viewr的调用会创建一个新的堆栈帧,然后在viewr的工作完成时立即弹出;然后调用者可以强制延迟值,从而为嵌套的viewr调用生成一个新的堆栈帧,然后立即再次弹出,等等,因此重复此过程不会导致堆栈溢出。