【问题标题】:Program design in Haskell: how to do simulation without mutabilityHaskell 中的程序设计:如何在没有可变性的情况下进行模拟
【发布时间】:2012-03-21 21:04:24
【问题描述】:

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

while True:
  simulationState = stepForward(simulationState)
  render(simulationState)

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

提前致谢!

【问题讨论】:

    标签: haskell simulation


    【解决方案1】:

    在我看来,思考这个问题的正确方法不是循环,而是列表或其他类似的无限流式结构。我把a similar answer 给了a similar question;基本思想是,作为C. A. McCann wrote,使用iterate stepForward initialState,其中iterate :: (a -> a) -> a -> [a]“返回[stepForward]到[initialState]的重复应用的无限列表”。

    这种方法的问题在于您无法处理 monadic 步骤,尤其是 monadic 渲染函数。一种方法是提前获取所需的列表块(可能使用takeWhile 之类的函数,可能使用手动递归),然后使用mapM_ render。更好的方法是使用不同的、本质上一元的流式结构。我能想到的四个是:

    • The iteratee package,最初是为流式 IO 设计的。我认为在这里,您的步骤将是一个源(enumerator),您的渲染将是一个接收器(iteratee);然后,您可以使用管道(enumeratee)在中间应用函数和/或进行过滤。
    • The enumerator package,基于相同的想法;一个可能比另一个更干净。
    • The newer pipes package,自称是“迭代器做得对”——它更新了,但语义至少对我来说明显更清晰,名称也是如此(Producer、Consumer 和 Pipe)。
    • The List package,尤其是它的 ListT monad 转换器。这个 monad 转换器旨在允许您创建比[m a] 更有用的结构的 monadic 值列表;例如,使用无限单子列表变得更易于管理。该包还将列表上的许多功能概括为a new type class。它提供了两次iterateM 函数; first time 具有令人难以置信的通用性,而 second time 专门用于 ListT。然后,您可以使用 takeWhileM 等函数进行过滤。

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

    【讨论】:

    • 您的列表似乎缺少the monad-loops package,我认为这实际上是该方法最清晰的演示。
    • 太棒了——我一直在寻找学习迭代的理由。我来看看管道包。非常感谢!
    • 对于最初的问题来说这有点过分了,但为了那些可能会来的人,我认为我们应该提到Functional Reactive Programming,尤其是Yampa/Animas。
    • @C.A.McCann:该软件包似乎采用了稍微不同的方法(基于组合器而不是基于数据结构),我认为您的答案无论如何都涵盖得更好。 (这个包也缺少我能找到的任何iterate-type 组合子。)
    • @AntalS-Z: 是的,但我认为这实际上是相同的底层方法——具体化来自这些组合器的递归与ListT 的关系与Data.List 中的递归组合器大致相同与普通列表有关;同样,它们强调递归和最终结果,而流处理则强调中间步骤的各个方面。我认为,了解每一个都可以更好地了解正在发生的事情。
    【解决方案2】:

    好吧,如果绘制连续状态是您想要做的全部,那非常简单。首先,获取您的step 函数和初始状态并使用the iterate function。 iterate step initialState 是每个模拟状态的(无限)列表。然后,您可以将 display 映射到其上以获取 IO 操作来绘制每个状态,因此您将拥有这样的东西:

    allStates :: [SimState]
    allStates = iterate step initialState
    
    displayedStates :: [IO ()]
    displayedStates = fmap display allStates
    

    运行它的最简单方法是使用the intersperse function 在每个显示动作之间放置一个“延迟”动作,然后使用the sequence_ function 运行整个过程:

    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)
    

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

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

    实现iterUntilM_ 函数留给读者作为练习,呵呵。

    【讨论】:

    • iterate/fmap 解决方案很棒,但我将使用recusion 方法。非常感谢!
    【解决方案3】:

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

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

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

    【讨论】:

    • 只是为了确认,这不会堆栈溢出,因为它是尾递归?
    • 它既不是尾递归也不应该堆栈溢出 :) 试一试,或者尝试对渲染状态列表进行排序的其他解决方案之一。
    • @haldean 它不会溢出堆栈,尽管出于不同的原因。由于惰性,尾递归在 Haskell 中不如在其他语言中有用或重要。
    猜你喜欢
    • 2021-03-31
    • 2022-11-17
    • 2022-11-19
    • 1970-01-01
    • 2023-03-12
    • 2012-02-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多