【问题标题】:Stack overflow exception when using pipes in tail-recursive function在尾递归函数中使用管道时出现堆栈溢出异常
【发布时间】:2016-10-20 20:20:05
【问题描述】:

我有一个简单的游戏循环实现

let gameLoop gamestate =     
    let rec innerLoop prev gamestate =
        let now = getTicks()
        let delta = now - prev
        gamestate 
        |> readInput delta
        |> update delta
        |> render delta
        |> innerLoop delta             

    innerLoop 0L gamestate 

此实现会引发 stackoverflow 异常。在我看来,这应该是尾递归的。我可以像这样解决问题

let gameLoop gamestate =     
    let rec innerLoop prev gamestate =
        let now = getTicks()
        let delta = now - prev
        let newState = gamestate 
            |> readInput delta
            |> update delta
            |> render delta

        innerLoop now newState

    innerLoop 0L gamestate  

所以我的问题是为什么第一个代码示例会引发 stackoverflow 异常。

【问题讨论】:

  • 你在哪个平台上运行?
  • 澄清一下:您发布的变通版本是否可以正常工作?
  • @FyodorSoikin 我使用 fsi 版本 14.0.23413.0 在 Windows 10 上运行
  • @Peter 是的,我的解决方法是有效的。
  • This question 可能在这里感兴趣。无论如何,如果错误发生在调试或发布编译中,以及是否附加调试器是否重要,这可能会很有趣。

标签: f# tail-recursion


【解决方案1】:

我认为答案与线程Vandroiy链接中的答案相同:当你有

a
|> f b

然后在调试模式下,编译器可能会像一个非常字面的解释一样编译它

(f b) a

并在一步中显式计算f b,并在第二步中将其应用于a。带有参数a 的调用仍然是尾调用,但如果编译器没有发出tail. 操作码前缀(因为尾调用已关闭,因为它们默认处于调试模式),那么您将增长显式调用堆栈并最终导致堆栈溢出。

另一方面,如果你写

f b a

直接那么这不会发生:编译器不会部分应用f,而是会识别这是一个直接递归调用并将其优化为一个循环(即使在调试模式下)。

【讨论】:

  • 我想我会选择这个遮阳篷。它最受好评,对我来说很有意义,我可以在 Visual Studio 中重现它。
【解决方案2】:

认为这就是解释,不过我鼓励 F# 编译器专家在我偏离基础时参与进来:

第一个示例不是尾递归的,因为尾部位置的表达式是对|> 的调用,而不是对innerLoop 的调用。

回想一下 |> 被定义为

let (|>) x f = f x

如果我们在调用时对管道语法稍加脱糖

gamestate 
    |> readInput delta
    |> update delta
    |> render delta
    |> innerLoop delta

你实际上是在打电话:

|> (innerLoop delta) (|> (render delta) (|> (update delta) (|> (readInput delta) gamestate)))

作为递归函数中的身体表达。

中缀符号稍微掩盖了这一点,使它看起来像innerLoop 在尾部位置。

【讨论】:

  • 哇,@Vandroiy,这是一个可怕的问题/答案线程。而且很难分析它以得出结论,除了——就像你说的——管道可能会影响 TCO。
  • 是的,但是innerLoop delta|> 的尾部位置,不是吗?所以innerLoop 应该tailcall |>,然后应该tailcall innerLoop 或者我错过了什么?
  • 啊,对不起,我在回复之前删除了我的评论。为清楚起见,已删除的评论链接到 this thread,我现在也将其发布在问题的 cmets 中。
  • 注意实际定义是let inline (|>) x f = f x(见prim-types.fs)。由于定义是inline,因此根本不应该在输出中调用运算符。
  • @Peter 我不是在谈论 F# 编译器检测到 |> 调用 innerLoop 回来。我说的是它生成尾调用指令而不是调用指令,因为对 |> 的调用是尾调用。
猜你喜欢
  • 2016-02-19
  • 1970-01-01
  • 1970-01-01
  • 2018-05-28
  • 2010-12-07
  • 2020-03-06
  • 2017-09-27
  • 1970-01-01
相关资源
最近更新 更多