【问题标题】:Functional Programming, recursing a game state loop函数式编程,递归游戏状态循环
【发布时间】:2012-08-17 17:11:08
【问题描述】:

我打算写一个简单的游戏来测试我对函数式编程的理解。执行主循环的功能方式是递归它,但这不会随着越来越多的堆栈帧生成而消耗内存吗?

谢谢

来自How can you do anything useful without mutable state?的例子

// imperative version
pacman = new pacman(0, 0)
while true
    if key = UP then pacman.y++
    elif key = DOWN then pacman.y--
    elif key = LEFT then pacman.x--
    elif key = UP then pacman.x++
    render(pacman)

// functional version
let rec loop pacman =
    render(pacman)
    let x, y = switch(key)
        case LEFT: pacman.x - 1, pacman.y
        case RIGHT: pacman.x + 1, pacman.y
        case UP: pacman.x, pacman.y - 1
        case DOWN: pacman.x, pacman.y + 1
    loop(new pacman(x, y))

【问题讨论】:

  • 你使用什么功能语言?
  • Clojure,但如果它取决于语言,我也会热衷于了解 F#。

标签: recursion functional-programming


【解决方案1】:

您已经使用tail recursion 实现了您的loop 函数,即对loop 的递归调用是函数中的最后一件事。这允许编译器/解释器(取决于语言)立即弹出当前堆栈帧并将其替换为递归调用的帧。

长话短说,按照你实现的方式,不会有堆栈溢出,loop 可以运行多久就可以。

【讨论】:

  • 如需进一步阅读,请参阅en.wikipedia.org/wiki/Continuation-passing_style
  • 好的,所以这可以假设在大多数语言中都会发生(甚至是命令式的)?如果递归调用不是最后一件事,我有什么办法可以做到这一点?
  • @Peter 通常不会对命令式语言执行此优化。它也不适用于 Clojure,但作为替代方案,Clojure 提供了 recur 形式,您可以使用它代替当前函数的名称进行递归而不消耗堆栈空间(recur 只能用于尾部位置) .如果递归调用不在尾部位置,则只能通过重写来避免消耗堆栈空间,使其处于尾部位置(请注意,如果您不能轻易做到这一点,您也将无法将逻辑简单地表达为没有显式堆栈的循环)。
  • @Peter 重要的是调用者必须在调用被调用者后立即返回。像这样的东西也是尾递归的:if x then loop(...) else loop(...);但不是这个:if x then (loop(...); 17) else loop(...)
【解决方案2】:

递归是新的迭代 :) 博客插件:http://blogs.msdn.com/b/ashleyf/archive/2010/02/06/recursion-is-the-new-iteration.aspx

您说您正在使用 Clojure,并且还热衷于了解 F#。

事实证明,基于 JVM 的语言(Java、Scala、Clojure 等)无法支持 VM 级别的尾调用优化,因此有诸如 Clojure 的recur 之类的变通方法。基于 CLR 的语言(F#、C#、VB...)可以并且确实在 IL 中使用 .tail 标记来导致堆栈帧的提前丢弃。

尾调用优化会使调试变得痛苦,例如 F# 在调试版本中不会这样做(但在发布版本中会这样做)。项目设置中有一个复选框可以在调试中启用。

【讨论】:

  • 请注意,JVM 语言不支持一般尾 call 优化,但可以支持更受限的尾 recursion 优化。斯卡拉确实如此。使用像 recur 这样的显式尾递归运算符的原因是,如果您期望的尾递归实际上最终不是这样,那么您会收到一个错误提示您注意这一事实,而如果隐式尾递归发生这种情况消除您可能(最终)收到有关本不应该发生的堆栈溢出的错误报告。
  • 是的,当然你可以做一个分支而不是调用简单(非相互)递归。当您想要进行调用并消除堆栈框架时,您对 JVM 不走运。