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 很陌生,所以不确定我哪里出错了,或者我上面的假设是否正确,似乎正在执行整个无限列表。
(更新)如果有帮助,这里是 Frame
、next
和 runSimulation
的完整定义:
-- 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
的准线性函数,正如预期的那样。
更多关于随机数生成的单子动作 。
我正在使用 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 很陌生,所以不确定我哪里出错了,或者我上面的假设是否正确,似乎正在执行整个无限列表。
(更新)如果有帮助,这里是 Frame
、next
和 runSimulation
的完整定义:
-- 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
的准线性函数,正如预期的那样。
更多关于随机数生成的单子动作