在尾递归函数中使用管道时出现堆栈溢出异常

时间:2016-10-20 20:20:05

标签: f# tail-recursion

我有一个天真的游戏循环实现

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 

此实现抛出stackoverflowexception。在我看来,这应该是尾递归。我可以像这样做一个工作

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异常。

2 个答案:

答案 0 :(得分:10)

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

a
|> f b

然后在调试模式下,编译器可以像对

的字面解释一样编译它
(f b) a

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

另一方面,如果你写

f b a
直接

然后就不会发生这种情况:编译器不会部分应用f,而是会认识到这是一个直接递归调用并将其优化为循环(即使在调试模式下)。

答案 1 :(得分:5)

认为这是解释,但我鼓励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处于尾部位置。