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

Haskell中涉及IO时如何实现惰性迭代,haskell,io,lazy-evaluation,Haskell,Io,Lazy Evaluation,我用IO来封装随机性。我正在尝试编写一个方法,该方法将下一个函数迭代n次,但由于随机性,下一个函数将生成一个封装在IO中的结果 基本上,我的下一个函数具有以下特征: 下一步::IO帧->IO帧 我想从一个初始帧开始,然后使用与iterate相同的模式得到一个长度为n的列表[Frame]。基本上,我希望能够写出以下内容: 运行模拟::{-parameters-}->IO[帧] 运行模拟{-parameters-}=do {some setup-} 序列以n为例。迭代下一个$firstFrame 其

我用IO来封装随机性。我正在尝试编写一个方法,该方法将下一个函数迭代n次,但由于随机性,下一个函数将生成一个封装在IO中的结果

基本上,我的下一个函数具有以下特征:

下一步::IO帧->IO帧 我想从一个初始帧开始,然后使用与iterate相同的模式得到一个长度为n的列表[Frame]。基本上,我希望能够写出以下内容:

运行模拟::{-parameters-}->IO[帧] 运行模拟{-parameters-}=do {some setup-} 序列以n为例。迭代下一个$firstFrame 其中firstFrame::IO Frame是通过执行类似于let firstFrame=返回帧x y z的操作形成的

我遇到的问题是,当我运行这个函数时,它永远不会退出,因此它似乎运行在一个无限循环上,因为iterate生成一个无限列表

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

如果有帮助,请更新,以下是Frame、next和runSimulation的完整定义:

-模拟帧封装了某一时刻的模拟状态 -时间点。这意味着它包含一个代理列表 -框架,以及其中发生的交互的列表。信息技术 -还包含世界的状态,以及一个AgentID计数器 -因此,我们可以轻松地增加生成新代理的数量。 数据帧=帧代理[代理][交互] 衍生节目 -从当前帧生成下一帧,包括对 -代理基于此框架中的结果*。 -TODO:加载项复制。 下一帧::反应器->世界->IO帧->IO帧 下一帧反应w inp=do 帧i代理历史记录距离,距离->IO[帧] runSimulation world react gen_dist,sel_dist=do
startingAgents我不知道计算每帧需要多少时间,但我怀疑您做的工作超出了需要。原因有点微妙。迭代生成函数的重复应用程序列表。对于列表中的每个元素,将重用上一个值。您的列表由IO操作组成。通过应用next,从位置n-1处已获得的IO操作计算位置n处的IO操作

唉,当我们执行这些行动时,我们就没那么幸运了。在列表中的位置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)
虽然很简单,但有点不雅,原因有二:

它将迭代过程与这种过程的限制混为一谈。在纯粹的列表世界中,我们不必这样做,我们可以创建无限的列表并从中获取。但现在,在这个有效的世界里,这种美妙的构图似乎消失了

如果我们想在生产过程中对每一个价值进行处理,而不必等待所有价值,那该怎么办?Out函数在最后一次性返回所有内容

正如在评论中提到的,流媒体库喜欢,或者试图以更好的方式解决这个问题,重新获得一些纯列表的组合性。这些库具有表示有效进程的类型,这些进程的结果是分段生成的

例如,考虑流函数,专门针对IO:

iterateM::a->IO a->IO a->IO r的流

它返回一个我们可以使用以下方法限制的流:

take::Int->IO流r->IO流

限制它之后,我们可以返回IO[a],用它累积所有结果:

toList\:输入输出流->输入输出[a]

但是,我们可以在生成每个元素时对其进行处理,其功能如下:

映射:a->IO x->IO r流->IO r


我不知道计算每个帧需要多少时间,但我怀疑您所做的工作超出了必要的范围。原因有点微妙。迭代生成函数的重复应用程序列表。对于列表中的每个元素,将重用上一个值。您的列表由IO操作组成。通过应用next,从位置n-1处已获得的IO操作计算位置n处的IO操作

唉,当我们执行这些行动时,我们就没那么幸运了。在列表中的位置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)
虽然很简单,但有点不雅,原因有二:

It c 在迭代过程的限制下,对迭代过程进行平坦化。在纯粹的列表世界中,我们不必这样做,我们可以创建无限的列表并从中获取。但现在,在这个有效的世界里,这种美妙的构图似乎消失了

如果我们想在生产过程中对每一个价值进行处理,而不必等待所有价值,那该怎么办?Out函数在最后一次性返回所有内容

正如在评论中提到的,流媒体库喜欢,或者试图以更好的方式解决这个问题,重新获得一些纯列表的组合性。这些库具有表示有效进程的类型,这些进程的结果是分段生成的

例如,考虑流函数,专门针对IO:

iterateM::a->IO a->IO a->IO r的流

它返回一个我们可以使用以下方法限制的流:

take::Int->IO流r->IO流

限制它之后,我们可以返回IO[a],用它累积所有结果:

toList\:输入输出流->输入输出[a]

但是,我们可以在生成每个元素时对其进行处理,其功能如下:

映射:a->IO x->IO r流->IO r

基本解决方案: 作为@danidiaz答案的替代方案,假设IO的作用可以最小化,则不需要借助流媒体等额外库就可以解决问题

大多数必需的代码都可以用类来编写,IO只是其中的一个实例。生成伪随机数不需要使用IO

所需的迭代函数可以这样编写,使用do表示法:

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

将所有代码放在一起:

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

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

有关随机数生成的一元操作的详细信息。

基本解决方案: 作为@danidiaz答案的替代方案,假设IO的作用可以最小化,则不需要借助流媒体等额外库就可以解决问题

大多数必需的代码都可以用类来编写,IO只是其中的一个实例。生成伪随机数不需要使用IO

所需的迭代函数可以这样编写,使用do表示法:

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

将所有代码放在一起:

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

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


有关随机数生成的一元操作的更多信息。

您可能需要提供更多有关next和/或runSimulation的上下文。序列以1为例。迭代id$return foo返回一个操作,该操作在执行时生成[foo]。请改用导管。它有一个更自然的接口来处理像这样与IO交织的流?您可能会得到一个组合爆炸,因为iterate返回的列表中的程序并不是真正基于上一个结果构建的。也就是说,位置6处的IO操作在执行时将再次执行操作1-5。您还可以尝试next::Frame->IO Frame并迭代>>=next FirstFrame您可能需要提供有关next和/或runSimulation的更多上下文。序列以1为例。迭代id$return foo返回一个操作,该操作在执行时生成[foo]。请改用导管。它有一个更自然的接口来处理像这样与IO交织的流?您可能会得到一个组合爆炸,因为iterate返回的列表中的程序并不是真正基于上一个结果构建的。也就是说,位置6处的IO操作在执行时将再次执行操作1-5。您也可以尝试next::Frame->IO Frame并迭代>>=next firstFrameOk我认为这是正确的,因为当我尝试只运行一两次迭代时,它会立即返回。我想我有点理解您对iterateM的实现,不过如果您能解释行current:go next pred limit,那就太好了。@GTF current:是一个操作符部分,基本上是编写函数\xs->current:xs的一种奇特方式。pred limit是limit-1。递归调用go next pred limit的类型为IO[a],它是生成其余元素的操作。是fmap的中缀版本,使用
在IO操作中工作,并将当前元素前置到列表的前面。因为我不需要在项目出现时对其进行操作,所以我现在跳过了流式处理方法,您的iterateM方法肯定有效。谢谢。我不明白的是,为什么不能编写一个函数来执行迭代步骤,将其存储为变量,并将其添加到列表和递归步骤中。这似乎对IO不起作用。我得到一个关于无限类型的错误。@GTF没有代码很难说。也许值得提出一个问题。好吧,这个问题暂时有效,但我可以单独写另一个问题。谢谢。我认为这是正确的,因为当我尝试只运行一两次迭代时,它会立即返回。我想我有点理解您对iterateM的实现,不过如果您能解释行current:go next pred limit,那就太好了。@GTF current:是一个操作符部分,基本上是编写函数\xs->current:xs的一种奇特方式。pred limit是limit-1。递归调用go next pred limit的类型为IO[a],它是生成其余元素的操作。是fmap的中缀版本,它用于在IO操作中工作,并将当前元素前置到列表的前面。因为我不需要在项目出现时对其进行操作,所以我现在跳过了流方法,并且您的iterateM方法确实有效。谢谢。我不明白的是,为什么不能编写一个函数来执行迭代步骤,将其存储为变量,并将其添加到列表和递归步骤中。这似乎对IO不起作用。我得到一个关于无限类型的错误。@GTF没有代码很难说。也许值得提出一个问题。好吧,这个问题暂时有效,但我可以单独写另一个问题。谢谢。为什么不能用IO而不是MonadRandom来编写iterateM1函数呢?当然,像使用那样提前计算do符号和结果,我没有添加太多关于next和Frame的细节,因为我认为这会对问题造成一点污染,但我会更新问题以添加更多细节。@GTF当然可以使用IO编写iterateM1。但正是IO的使用阻止了runSimulation函数正确退出。如果只使用随机数生成monad(本质上是状态monad)而不是IO,那么问题就消失了。IO monad对于您想要做的事情不够懒惰。注意,我已经导入了Control.Monad.Random.Lazy。您是否出于与随机数生成无关的原因需要IO?你用IO来封装随机性的这句话。我只需要随机性,我只是好奇IO为什么会有这种不同。@GTF IO是不同的,因为当现实世界的副作用出现时,Haskell不敢冒险重新排列这些副作用。你有一个IO[Frame]类型的表达式,Haskell会坚持做所有相关的副作用,让你使用结果;如果有无穷多的副作用,那就太糟糕了!您可以按照danidiaz的建议,将迭代次数作为一个额外参数添加,或者将随机性移回非IO纯计算的更安全领域,例如我在这里讨论的。为什么不能使用IO而不是MonadRandom来编写iterateM1函数?当然,像使用那样提前计算do符号和结果,我没有添加太多关于next和Frame的细节,因为我认为这会对问题造成一点污染,但我会更新问题以添加更多细节。@GTF当然可以使用IO编写iterateM1。但正是IO的使用阻止了runSimulation函数正确退出。如果只使用随机数生成monad(本质上是状态monad)而不是IO,那么问题就消失了。IO monad对于您想要做的事情不够懒惰。注意,我已经导入了Control.Monad.Random.Lazy。您是否出于与随机数生成无关的原因需要IO?你用IO来封装随机性的这句话。我只需要随机性,我只是好奇IO为什么会有这种不同。@GTF IO是不同的,因为当现实世界的副作用出现时,Haskell不敢冒险重新排列这些副作用。你有一个IO[Frame]类型的表达式,Haskell会坚持做所有相关的副作用,让你使用结果;如果有无穷多的副作用,那就太糟糕了!您可以按照danidiaz的建议,将迭代次数添加为一个额外参数,或者将随机性移回非IO纯计算的更安全领域,例如我在这里讨论的。
$ ./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
$