【问题标题】:How can I get this function to be tail-recursive?我怎样才能让这个函数是尾递归的?
【发布时间】:2017-03-23 11:13:51
【问题描述】:

我是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 绝对不是尾递归的。为什么它不总是爆炸?如何使用延续来重写它?

【问题讨论】:

  • 懒惰确实意味着实际上根本没有递归调用——问题是这个函数和它的调用者之间的交互。能否也提供调用代码?
  • @kvb:完成。如果您需要查看更多内容,请在开头链接整个 repo。
  • 在 Windows 上的 F# Interactive (x64) 中,我可以在具有 1M 个元素的树上调用 toList,而不会出现堆栈溢出。
  • 也许您遇到了与here 相同的问题?尝试移除管道。

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


【解决方案1】:

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

【讨论】:

    猜你喜欢
    • 2014-01-07
    • 1970-01-01
    • 2014-01-05
    • 2021-10-06
    • 2016-03-07
    • 1970-01-01
    • 2010-10-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多