Haskell中的程序设计:如何在没有可变性的情况下进行模拟

Hal*_*own 23 simulation haskell

我有一个关于设计我正在Haskell中工作的程序的最佳方法的问题.我正在写一个物理模拟器,这是我在标准命令式语言中做过的事情,通常主要的方法看起来像:

while True:
  simulationState = stepForward(simulationState)
  render(simulationState)
Run Code Online (Sandbox Code Playgroud)

我想知道如何在Haskell中做类似的事情.我有一个函数step :: SimState -> SimState和一个display :: SimState -> IO ()使用HOpenGL来绘制模拟状态的函数,但我不知道如何在各种"循环"中执行此操作,因为我可以提出的所有解决方案都涉及某种类型可变性 当谈到Haskell时,我有点像菜鸟,所以我完全有可能错过了一个非常明显的设计决定.此外,如果有一个更好的方法来构建我的整个程序,我会很高兴听到它.

提前致谢!

C. *_*ann 21

好吧,如果绘制连续状态是所有你想做的事,这是非常简单的.首先,获取您的step函数和初始状态并使用iterate函数.iterate step initialState然后是每个模拟状态的(无限)列表.然后你可以映射display它以获得IO动作来绘制每个状态,所以你可以一起得到这样的东西:

allStates :: [SimState]
allStates = iterate step initialState

displayedStates :: [IO ()]
displayedStates = fmap display allStates
Run Code Online (Sandbox Code Playgroud)

运行它的最简单方法是使用intersperse函数在每个显示操作之间设置"延迟"操作,然后使用sequence_函数运行整个操作:

main :: IO ()
main = sequence_ $ intersperse (delay 20) displayedStates
Run Code Online (Sandbox Code Playgroud)

当然,这意味着您必须强制终止应用程序并排除任何类型的交互性,因此这通常不是一个好方法.

一种更明智的方法是在每一步中交错诸如"查看应用程序是否应该退出"之类的内容.你可以用显式递归来做到这一点:

runLoop :: SimState -> IO ()
runLoop st = do display st
                isDone <- checkInput
                if isDone then return () 
                          else delay 20 >> runLoop (step st)
Run Code Online (Sandbox Code Playgroud)

我首选的方法是编写非递归步骤,然后使用更抽象的循环组合器.不幸的是,在标准库中以这种方式做到并不是很好的支持,但它看起来像这样:

runStep :: SimState -> IO SimState
runStep st = do display st
                delay 20
                return (step st)

runLoop :: SimState -> IO ()
runLoop initialState = iterUntilM_ checkInput runStep initialState
Run Code Online (Sandbox Code Playgroud)

实现这个iterUntilM_功能留给读者练习,呵呵.


Ant*_*sky 21

在我看来,思考这个问题的正确方法不是作为循环,而是作为列表或其他这样的无限流结构.我给了一个类似的答案,以类似的问题 ; 正如CA McCann写的那样,基本思想是使用iterate stepForward initialState,其中iterate :: (a -> a) -> a -> [a]"返回[ stepForward]到[ initialState] 的重复应用的无限列表.

这种方法的问题在于您在处理monadic步骤时遇到问题,尤其是monadic渲染函数.一种方法就是提前获取列表中所需的块(可能具有类似的功能takeWhile,可能还有手动递归),然后再mapM_ render进行操作.更好的方法是使用不同的,本质上是monadic的流式结构.我能想到的四个是:

  • iteratee包,最初是为流式IO设计的.我想在这里,你的步骤将是一个源(enumerator),你的渲染将是一个接收器(iteratee); 然后你可以使用管道(an enumeratee)来应用函数和/或在中间进行过滤.
  • 枚举器包,基于相同的想法; 一个可能比另一个更干净.
  • 较新的管道包,其自称是"iteratees做得正确" -它是新的,但语义是,至少对我来说,更清晰显著,因为是名字(Producer,Consumer,和Pipe).
  • List包,特别是它的ListTmonad变换器.这个monad变换器旨在允许您创建具有更多有用结构的monadic值列表[m a]; 例如,使用无限monadic列表变得更易于管理.该包还将列表上的许多函数概括为新类型.它提供iterateM两次功能; 在第一次令人难以置信的通用性,并且第二次专门到ListT.然后,您可以使用诸如takeWhileM进行过滤之类的功能.

在一些数据结构中重复程序迭代而不是简单地使用递归的最大好处是,您的程序可以使用控制流做有用的事情.当然,没有什么太夸张的了,但是,例如,它将"如何终止"决定与"如何生成"过程分开.现在,用户(即使只是你)可以单独决定何时停止:在n步之后?在状态满足某个谓词后?没有理由用这些决定来掩盖你的生成代码,因为它在逻辑上是一个单独的问题.

  • 这对于原始问题来说是过度的,但是为了那些可能来的人,我认为我们应该提到[功能反应式编程](http://stackoverflow.com/questions/3154701/help-understanding-arrows-in-haskell特别是[Yampa/Animas](http://www.haskell.org/haskellwiki/Yampa). (3认同)

Ing*_*ngo 12

你的方法没问题,你只需要记住循环在Haskell中表示为递归:

simulation state = do
    let newState = stepForward state
    render newState
    simulation newState
Run Code Online (Sandbox Code Playgroud)

(但你肯定需要一个如何结束循环的标准.)

  • @haldean尽管出于不同的原因,它不会溢出堆栈.由于懒惰,尾递归在Haskell中并不像在其他语言中那样有用或重要. (7认同)
  • 它既不是尾递归也不应该堆栈溢出:)尝试一下,或尝试其中一个对呈现状态列表进行排序的其他解决方案. (3认同)