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

Haskell中的程序设计:如何在不可变的情况下进行模拟,haskell,simulation,Haskell,Simulation,我有一个关于设计我在Haskell工作的程序的最佳方法的问题。我正在编写一个物理模拟器,这是我用标准命令式语言做过的一系列工作,通常主要方法如下: while True: simulationState = stepForward(simulationState) render(simulationState) 我想知道如何在Haskell做类似的事情。我有一个函数step::SimState->SimState和一个函数display::SimState->IO(),它使用HOpen

我有一个关于设计我在Haskell工作的程序的最佳方法的问题。我正在编写一个物理模拟器,这是我用标准命令式语言做过的一系列工作,通常主要方法如下:

while True:
  simulationState = stepForward(simulationState)
  render(simulationState)
我想知道如何在Haskell做类似的事情。我有一个函数
step::SimState->SimState
和一个函数
display::SimState->IO()
,它使用HOpenGL绘制模拟状态,但我不知道如何在某种“循环”中这样做,因为我能想到的所有解决方案都涉及某种可变性。说到Haskell,我有点不知所措,所以我完全有可能错过了一个非常明显的设计决策。另外,如果有更好的方法来设计我的程序作为一个整体,我很高兴听到它


提前谢谢

您的方法还可以,只需记住循环在Haskell中表示为递归:

simulation state = do
    let newState = stepForward state
    render newState
    simulation newState

(但您确实需要一个如何结束循环的标准。)

好吧,如果您只想绘制连续状态,那就相当简单了。首先,使用
步骤
函数和初始状态并使用<代码>迭代步骤初始状态是每个模拟状态的(无限)列表。然后,您可以将
display
映射到该图上,以获得绘制每个状态的IO操作,这样您就可以得到如下结果:

allStates :: [SimState]
allStates = iterate step initialState

displayedStates :: [IO ()]
displayedStates = fmap display allStates
runStep :: SimState -> IO SimState
runStep st = do display st
                delay 20
                return (step st)

runLoop :: SimState -> IO ()
runLoop initialState = iterUntilM_ checkInput runStep initialState
运行它的最简单方法是在每个显示操作之间放置一个“延迟”操作,然后使用运行整个过程:

main :: IO ()
main = sequence_ $ intersperse (delay 20) displayedStates
当然,这意味着您必须强制终止应用程序,并排除任何形式的交互,因此一般来说,这并不是一个好方法

一种更明智的方法是在每一步中交替进行“查看应用程序是否应该退出”之类的操作。您可以通过显式递归实现这一点:

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

实现
iterUntilM\uu
功能留给读者作为练习,呵呵。

在我看来,思考这个问题的正确方式不是循环,而是列表或其他类似的无限流结构。我给予;基本思想是,使用
迭代stepfroward initialState
,其中“返回[
stepfroward
]到[
initialState
]的重复应用程序的无限列表”

这种方法的问题在于处理一元步骤时有困难,尤其是一元渲染函数。一种方法是预先获取所需的列表块(可能使用类似于
takeWhile
的函数,可能使用手动递归),然后在此基础上进行
mapM\urender
。更好的方法是使用不同的、本质上是一元的流结构。我能想到的四点是:

  • ,它最初是为流式IO设计的。我认为在这里,您的步骤将是一个源(
    enumerator
    ),而您的呈现将是一个接收器(
    iteratee
    );然后可以使用管道(<代码>枚举数< /COD>)在中间应用函数和/或进行过滤。
  • ,基于相同的想法;一个可能比另一个干净
  • ,它自称“iteratees doe right”-它较新,但至少对我来说,语义更清晰,名称(
    生产者
    消费者
    ,和
    管道
    )也是如此
  • 尤其是它的
    ListT
    monad转换器。此单声道转换器旨在允许您创建比
    [ma]
    更有用的单声道值列表;例如,处理无限的单子列表变得更容易管理。该包还将列表中的许多函数概括为。它提供了两次
    iterateM
    函数;这种方法具有难以置信的通用性,并且专门用于
    ListT
    。然后,您可以使用诸如之类的函数进行过滤

在某些数据结构中具体化您的程序的迭代,而不是简单地使用递归,最大的优点是您的程序可以使用控制流做有用的事情。当然,没有什么太夸张的,但例如,它将“如何终止”决策与“如何生成”过程分开。现在,用户(即使只是您)可以分别决定何时停止:n步之后?在状态满足某个谓词之后?没有理由因为这些决定而使生成代码陷入困境,因为这在逻辑上是一个独立的问题。

只是确认一下,这不会因为是尾部递归而导致堆栈溢出?它既不是尾部递归,也不应该是堆栈溢出:)试试看,或者尝试其他对呈现状态列表进行排序的解决方案。@haldean它不会溢出堆栈,尽管原因不同。由于懒惰,尾部递归在Haskell中不如在其他语言中有用或重要。iterate/fmap解决方案很棒,但我将使用递归方法。非常感谢!您的列表似乎缺失了,我认为这实际上是该方法最清晰的演示。太棒了——我一直在寻找学习iteratees的理由。我来看看管道包装。非常感谢!对于最初的问题来说,这太过分了,但对于那些可能在后面的人来说,我认为我们应该特别提及。@C.A.McCann:该软件包似乎采取了一种稍微不同的方法(基于组合器的方法,而不是基于数据结构的方法),我认为您的答案涵盖得更好。(这个包也缺少我能找到的任何
迭代
类型的组合词。)@AntalS-Z:是的,但我认为这实际上是相同的基本方法——从这些组合词中具体化递归与
列表
的关系,大致与
数据中的递归组合词的关系相同;同样,它们强调递归和最终结果,即