Haskell涉及IO时如何实现惰性迭代

How to implement lazy iterate when IO is involved in Haskell

我正在使用 IO 来封装随机性。我正在尝试编写一种将 next 函数迭代 n 次的方法,但由于随机性,next 函数会产生一个包含在 IO 中的结果。

基本上,我的 next 函数具有以下签名:

next :: IO Frame -> IO Frame

我想从初始 Frame 开始,然后使用与 iterate 相同的模式来获得长度为 n 的列表 [Frame]。本质上,我希望能够编写以下内容:

runSimulation :: {- parameters -} -> IO [Frame]
runSimulation {- parameters -} = do
  {- some setup -}
  sequence . take n . iterate next $ firstFrame

其中 firstFrame :: IO Frame 通过执行 let firstFrame = return Frame x y z.

之类的操作形成

我遇到的问题是,当我 运行 这个函数时,它永远不会退出,所以它 似乎 是 运行 无限期循环(因为 iterate 产生一个无限列表)。

我对 haskell 很陌生,所以不确定我哪里出错了,或者我上面的假设是否正确,似乎正在执行整个无限列表。


(更新)如果有帮助,这里是 FramenextrunSimulation 的完整定义:

-- A simulation Frame encapsulates the state of the simulation at some
-- point in "time". That means it contains a list of Agents in that
-- Frame, and a list of the Interactions that occurred in it as well. It
-- also contains the state of the World, as well as an AgentID counter
-- (so we can easily increment for generating new Agents).
data Frame = Frame AgentID [Agent] [Interaction]
  deriving Show

-- Generate the next Frame from the current one, including scoring the
-- Agents based on the outcomes *in this Frame*.
-- TODO: add in reproduction.
nextFrame :: Reactor -> World -> IO Frame -> IO Frame
nextFrame react w inp = do
  (Frame i agents history) <- inp
  interactions <- interactAll react history agents
  let scoredAgents = scoreAgents (rewards w) interactions agents
  return (Frame i scoredAgents interactions)

-- Run a simulation for a number of iterations
runSimulation :: World -> Reactor -> (Dist, Dist) -> IO [Frame]
runSimulation world react (gen_dist, sel_dist) = do
  startingAgents <- spawnAgents (initial_size world) (agentCreatorFactory gen_dist sel_dist)
  let firstFrame = return (Frame (length startingAgents) startingAgents [])
      next       = nextFrame react world
  sequence . take (iterations world) . iterate next $ firstFrame

我不知道计算每个 Frame 需要多少时间,但我怀疑您做的工作超出了必要。原因有点微妙。 iterate 生成函数重复应用的列表。对于列表中的每个元素,都会重复使用之前的值。您的列表由 IO 个操作组成。位置 n 处的 IO 动作是根据位置 n-1 处已经获得的 IO 动作通过应用 next.

唉,当执行那些动作的时候,我们就没那么幸运了。执行列表中n位置的动作会重复前面动作的所有工作!我们在构建操作本身时共享工作(它们是值,就像 Haskell 中的几乎所有内容一样)但在执行它们时不共享,这是另一回事。

最简单的解决方案可能是用内置限制定义此辅助函数:

iterateM :: Monad m => (a -> m a) -> a -> Int -> m [a]
iterateM step = go
    where
    go _ 0 = return []
    go current limit =
        do next <- step current
           (current:) <$> go next (pred limit)

虽然简单,但有点不雅,原因有二:

  • 它将迭代过程与该过程的限制混为一谈。在纯列表世界中,我们不必这样做,我们可以创建无限列表,然后 take。但是现在在有效的世界中,好的组合性似乎已经丢失了。

  • 如果我们想在生成每个值时对其进行处理,而不必等待所有值,该怎么办? out函数returns一切尽在最后,一气呵成


正如评论中提到的,像 "conduit", "streamly" or "streaming" 这样的流式库试图以更好的方式解决这个问题,重新获得纯列表的一些组合性。这些库具有表示有效过程的类型,其结果是分段产生的。

例如,考虑 "streaming" 中的函数 Streaming.Prelude.iterateM,特化为 IO:

iterateM :: (a -> IO a) -> IO a -> Stream (Of a) IO r

它 returns 一个 Stream 我们可以 "limit" 使用 Streaming.Prelude.take:

take :: Int -> Stream (Of a) IO r -> Stream (Of a) IO ()

在限制它之后我们可以回到 IO [a]Streaming.Prelude.toList_ 累积所有结果:

toList_ :: Stream (Of a) IO r -> IO [a]

但我们可以在生成每个元素时 处理每个元素 ,使用 Streaming.Prelude.mapM_:

等函数来代替

mapM_ :: (a -> IO x) -> Stream (Of a) IO r -> IO r

一个基本的解决方案:

作为@danidiaz 答案的替代方案,假设可以最小化 IO 的作用,无需借助额外的库(例如 Streaming)即可解决问题。

大部分需要的代码都可以写成MonadRandomclass,其中IO只是一个实例。不必为了生成伪随机数而使用IO。

需要的迭代函数可以这样写,用写法:

import  System.Random
import  Control.Monad.Random.Lazy

iterateM1 :: MonadRandom mr => (a -> mr a) -> a -> mr [a]
iterateM1 fn x0 = 
    do
        y  <- fn x0
        ys <- iterateM1 fn y
        return (x0:ys)

不幸的是,问题的文本没有准确定义 Frame 对象是什么,或者 next 步进函数的作用;所以我必须以某种方式填补空白。 next 名称也在所涉及的库中定义,因此我将不得不使用 nextFrame 而不仅仅是 next.

假设Frame对象只是3维中的一个点space,并且在每个随机步骤中,随机选择3个维度中的一个,对应的坐标是以相同的概率增加 +1 或 -1。 这给出了这个代码:

data Frame = Frame Int Int Int  deriving  Show

nextFrame :: MonadRandom mr => Frame -> mr Frame
nextFrame (Frame x y z) =
    do
        -- 3 dimensions times 2 possible steps: 1 & -1, hence 6 possibilities
        n <- getRandomR (0::Int, 5::Int)
        let fr = case n of
                  0 -> Frame (x-1) y z
                  1 -> Frame (x+1) y z
                  2 -> Frame x (y-1) z
                  3 -> Frame x (y+1) z
                  4 -> Frame x y (z-1)
                  5 -> Frame x y (z+1)
                  _ -> Frame x y z
        return fr

到那时,编写代码来构建代表模拟历史的无限帧对象列表并不困难。创建该列表不会导致代码永远循环,通常的 take 函数可用于 select 这种列表的前几个元素。

将所有代码放在一起:

import  System.Random
import  Control.Monad.Random.Lazy

iterateM1 :: MonadRandom mr => (a -> mr a) -> a -> mr [a]
iterateM1 fn x0 = 
    do
        y  <- fn x0
        ys <- iterateM1 fn y
        return (x0:ys)

data Frame = Frame Int Int Int  deriving  Show

nextFrame :: MonadRandom mr => Frame -> mr Frame
nextFrame (Frame x y z) =
    do
        -- 3 dimensions times 2 possible steps: 1 & -1, hence 6 possibilities
        n <- getRandomR (0::Int, 5::Int)
        let fr = case n of
                  0 -> Frame (x-1) y z
                  1 -> Frame (x+1) y z
                  2 -> Frame x (y-1) z
                  3 -> Frame x (y+1) z
                  4 -> Frame x y (z-1)
                  5 -> Frame x y (z+1)
                  _ -> Frame x y z
        return fr

runSimulation :: MonadRandom mr => Int -> Int -> Int -> mr [Frame]
runSimulation x y z  =  let  fr0 = Frame x y z  in  iterateM1 nextFrame fr0

main = do
    rng0 <- getStdGen  -- PRNG hosted in IO monad
                       -- Could use mkStdGen or MkTFGen instead
    let
         sim  = runSimulation 0 0 0
         allFrames = evalRand sim rng0   -- unlimited list of frames !
         frameCount = 10
         frames = take frameCount allFrames
    mapM_  (putStrLn  .  show)  frames

程序执行:

$ ./frame
Frame 0 0 0
Frame 0 1 0
Frame 0 0 0
Frame 0 (-1) 0
Frame 1 (-1) 0
Frame 1 (-2) 0
Frame 1 (-1) 0
Frame 1 (-1) 1
Frame 1 0 1
Frame 2 0 1
$ 


对于较大的 frameCount 值,执行时间是 frameCount 的准线性函数,正如预期的那样。

更多关于随机数生成的单子动作