我有一个天真的游戏循环实现
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异常。
答案 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
处于尾部位置。