当IO参与Haskell时如何实现延迟迭代 [英] How to implement lazy iterate when IO is involved in Haskell
问题描述
我正在使用IO
封装随机性.我正在尝试编写一种方法,该方法将next
函数进行n
次,但是由于随机性,next
函数会产生包装在IO中的结果.
I'm using IO
to encapsulate randomness. I am trying to write a method which iterates a next
function n
times, but the next
function produces a result wrapped in IO because of the randomness.
基本上,我的next
函数具有以下签名:
Basically, my next
function has this signature:
next :: IO Frame -> IO Frame
,我想从初始的Frame
开始,然后使用与iterate
相同的模式来获取长度为n
的列表[Frame]
.本质上,我希望能够编写以下内容:
and I want to start with an initial Frame
, then use the same pattern as iterate
to get a list [Frame]
with length n
. Essentially, I'd like to be able to write the following:
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
之类的方式形成的.
Where firstFrame :: IO Frame
formed by doing something like let firstFrame = return Frame x y z
.
我遇到的问题是,当我运行此函数时,它永远不会退出,因此它似乎在无限循环中运行(因为iterate
产生无限列表).
The problem I am encountering is that when I run this function, it never exits, so it seems to be running on an infinite loop (since iterate
produces an infinite list).
我对Haskell还是很陌生,所以不确定在这里我哪里出错了,或者不确定上面的假设是否正确,似乎整个无限列表都在执行.
I'm quite new to haskell so not sure where I'm going wrong here, or if my supposition above is correct that it seems that the entire infinite list is being executed.
(更新)如果有帮助,请参见Frame
,next
和runSimulation
的完整定义:
(Update) In case it's helpful, here are the full definitions of Frame
, next
, and 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
操作组成.通过应用next
,根据已经在位置 n-1 上获得的IO
动作,计算位置 n 上的IO
动作.
I don't know how much time computing each Frame
takes, but I suspect you are doing more work than necessary. The cause is a bit subtle. iterate
produces a list of repeated applications of a function. For each element in the list, the previous value is reused. Your list is composed of IO
actions. The IO
action at position n is computed from the already obtained IO
action at position n-1 by applying next
.
A,当执行这些动作时,我们不是很幸运.在列表中的位置 n 处执行操作将重复先前操作的所有工作!我们在构建动作本身(即价值,就像Haskell中的几乎所有内容)时共享了工作,但在执行它们时却没有,这是另一回事.
Alas, when executing those actions, we are not so lucky. Executing the action at position n in the list will repeat all the work of the previous actions! We shared work when building the actions themselves (which are values, like almost everything in Haskell) but not when executing them, which is a different thing.
最简单的解决方案可能是使用烘焙限制来定义此辅助功能:
The simplest solution could be to define this auxiliary function with a baked-in limit:
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)
虽然简单,但有点不雅致,原因有二:
While simple, it's a bit inelegant, for two reasons:
-
它在限制迭代过程的情况下使迭代过程更加紧凑.在纯列表世界中,我们不必这样做,我们可以从那时开始创建无限列表和
take
.但是现在在有效的世界中,似乎失去了很好的构图性.
It conflates the iteration process with the limiting of such process. In the pure list world we didn't have to do that, we could create infinite lists and
take
from then. But now in the effectful world that nice compositionality seems to be lost.
如果我们要在生成每个值时都做一些事情,而不必等待所有值,该怎么办? Out函数一次完成返回所有内容.
What if we want to do something with each value as it is being produced, without having to wait for all of them? Out function returns everything at the end, in one go.
如评论中所述,流式库如导管" ,"streamly" 或流式处理" 试图以更好的方式解决此问题,重新获得一些纯列表的组合性.这些库具有代表有效过程的类型,其结果是分段产生的.
As mentioned in the comments, streaming libraries like "conduit", "streamly" or "streaming" try to solve this problem in a better way, regaining some of the compositionality of pure lists. These libraries have types that represent effectful processes whose results are yielded piecewise.
例如,考虑函数 Streaming.Prelude.iterateM
从流"开始,专门用于IO
:
iterateM ::(a-> IO a)-> IO a->流(流)IO r
iterateM :: (a -> IO a) -> IO a -> Stream (Of a) IO r
It returns a Stream
that we can "limit" using Streaming.Prelude.take
:
take :: Int->流(一个)IO r->流(一个)IO()
take :: Int -> Stream (Of a) IO r -> Stream (Of a) IO ()
限制后,我们可以使用Streaming.Prelude.toList_
会累积所有结果:
after limiting it we can get back to IO [a]
with Streaming.Prelude.toList_
which accumulates all results:
toList_ ::流(流)IO r-> IO [a]
toList_ :: Stream (Of a) IO r -> IO [a]
But instead of that we could process each element as it is being produced, with functions like Streaming.Prelude.mapM_
:
mapM_ ::(a-> IO x)->流(a)IO r-> IO r
mapM_ :: (a -> IO x) -> Stream (Of a) IO r -> IO r
这篇关于当IO参与Haskell时如何实现延迟迭代的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!