【问题标题】:Should this sequence expression be tail-recursive?这个序列表达式应该是尾递归的吗?
【发布时间】:2010-07-16 13:33:21
【问题描述】:

这个 F# seq 表达式在我看来是尾递归的,但我遇到了堆栈溢出异常(启用了尾调用)。有人知道我错过了什么吗?

let buildSecondLevelExpressions expressions =
    let initialState = vector expressions |> randomize
    let rec allSeq state = seq {
        for partial in state do
            if count partial = 1
            then yield Seq.head partial
            if count partial > 1 || (count partial = 1 && depth (Seq.head partial) <= MAX_DEPTH) then
                let allUns = partial
                                |> pick false 1
                                |> Seq.collect (fun (el, rr) -> (createExpUnaries el |> Seq.map (fun bn -> add rr bn)))
                let allBins = partial  // Careful: this case alone produces result recursivley only if |numbers| is even (rightly!).
                                |> pick false 2
                                |> Seq.collect (fun (el, rr) -> (createExpBinaries el |> Seq.map (fun bn -> add rr bn)))
                yield! allSeq (interleave allBins allUns)
    }
    allSeq initialState

如果您想知道,尽管它不应该很重要,pick 用于生成序列中的元素组合,interleave 将来自 2 个序列的元素交错。 vectorResizeArray 的构造函数。

【问题讨论】:

  • yield! allSeq ... 在 for 循环中。它不是尾递归的。
  • @Yin: yield! 根本不是尾递归的,例如枚举let rec xs() = seq {yield! xs()} 也会导致堆栈溢出。
  • yield! is tail-recursive 当它处于尾部调用位置时,但它似乎在 Jon 发布的退化示例中不起作用(我认为是一个错误)。然而,在所有现实的例子中,这应该是可行的。

标签: f# stack-overflow tail-recursion


【解决方案1】:

正如 Gideon 所指出的,这不是尾递归,因为“状态”列表中还有其他元素需要处理。使这个尾递归并不简单,因为您需要一些应该处理的元素queue

以下伪代码显示了一种可能的解决方案。我添加了work 参数来存储剩余的工作。在每次调用中,我们只处理第一个元素。所有其他元素都添加到队列中。完成后,我们从队列中挑选更多工作:

let rec allSeq state work = seq { 
    match state with 
    | partial::rest -> 
        // Yield single thing to the result - this is fine
        if count partial = 1 then yield Seq.head partial 
        // Check if we need to make more recursive calls...
        if count partial > 1 || (* ... *) then 
            let allUns, allBins = // ...
            // Tail-recursive call to process the current state. We add 'rest' to 
            // the collected work to be done after the current state is processed
            yield! allSeq (interleave allBins allUns) (rest :: work)
        else
            // No more processing for current state - let's take remaining
            // work from the 'work' list and run it (tail-recursively)
            match work with 
            | state::rest -> yield! allSeq state rest
            | [] -> () //completed
    | _ -> 
        // This is the same thing as in the 'else' clause above. 
        // You could use clever pattern matching to handle both cases at once
        match work with 
        | state::rest -> yield! allSeq state rest
        | [] -> () } //completed

【讨论】:

  • @Tomas Petricek -- 这是续集吗?说真的,我正在尝试理解延续的概念,这对我来说似乎是一个。
  • 你测试了吗?我认为这是错误的:你永远不应该在 yield... 中递归...
  • @Jon:我没有测试它(它是非常不完整的 sn-p),但我认为 allSeq 的所有递归使用都处于尾调用位置并使用 @987654326 返回@ - 序列表达式编译器完全适合这种情况。
  • @Onorio:Continuations 类似,但它是一个更通用的概念——不是传递需要完成的事情的队列,而是传递一个应该运行的函数(在上面的示例中,work将是函数,我们将在完成当前处理时执行它)。在我的书 (manning.com/petricek) 中有更清晰的解释,但不幸的是,这部分在免费示例章节中不可用...
  • @Tomas:当然,如果这是真的,您将能够枚举 let rec xs() = seq {yield! xs()} 但堆栈溢出。
【解决方案2】:

我在 F# 中找不到序列表达式中的哪些调用位于尾部位置的定义,因此我强烈建议不要编写依赖于当前实现语义的代码,即这是未定义的行为。

例如,尝试枚举(例如应用Seq.length)以下序列会导致堆栈溢出:

let rec xs() = seq { yield! xs() }

但是,正如 Tomas 所指出的,以下方法确实有效:

let rec xs n = seq { yield n; yield! xs(n+1) }

我的建议是始终用Seq.unfold 替换递归序列表达式。在这种情况下,您可能希望累积要完成的工作(例如,当您递归到左分支时,您将右分支压入累加器中的堆栈)。

FWIW,即使the F# language reference 也搞错了。它提供了以下代码用于展平一棵树:

type Tree<'a> =
   | Tree of 'a * Tree<'a> * Tree<'a>
   | Leaf of 'a

let rec inorder tree =
    seq {
      match tree with
          | Tree(x, left, right) ->
               yield! inorder left
               yield x
               yield! inorder right
          | Leaf x -> yield x
    } 

他们自己的代码在左侧输入一棵深树时,会因堆栈溢出而终止 F# 交互。

【讨论】:

  • 我认为你错了:yield!,让!,做!可以是尾递归的,只是在这种情况下不是。不过,为 Seq.unfold +1,谢谢。
  • @Mau:Tomas 给了我一个反例,递归调用可以在序列表达式的尾部位置。但是,如果没有任何关于何时发生这种情况的线索,我绝对会避免依赖它。
  • @Jon:我认为您发布的案例可能被认为是 F# 中的一个错误,因为应该优化尾部调用位置的所有 yield!。我会将链接发送给 F# 团队。
  • @Jon:我不明白你说的“F# 语言参考出错了” 指的是什么。它有什么问题?它并没有声称该解决方案不会堆栈溢出(我认为这对任何函数式程序员来说都不足为奇)。
  • @Jon, @Tomas - 一般来说,“尾递归”是否正常工作取决于正在使用的计算构建器。我想说 Jon 的病态示例确实体现了编译器错误,因为使用规范的 SequenceBuilder 定义而不是 seq 会导致无限循环,但不会导致堆栈溢出。
【解决方案3】:

这不会是尾递归,因为您可能会递归调用多次。翻译成伪代码:

allSeq(state)
{
    foreach (partial in state)
    {
        if (...)
        {
            yield ...
        }
        if (...)
        {
            ...
            //this could be reached multiple times
            yield! allSeq(...)
        }
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-10-20
    • 2020-05-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多