首页 文章

如何通过IO操作在某些非IO monad中惯用且高效地使用Pipe?

提问于
浏览
10

我有 Producer 使用我自己的 Random monad创建依赖于随机性的值:

policies :: Producer (Policy s a) Random x

Randommwc-random 的包装器,可以从 STIO 运行:

newtype Random a =
  Random (forall m. PrimMonad m => Gen (PrimState m) -> m a)

runIO :: Random a -> IO a
runIO (Random r) = MWC.withSystemRandom (r @ IO)

policies 生产环境 者通过简单的强化学习算法产生更好和更好的策略 .

通过索引到 policies ,我可以有效地绘制政策,比如5,000,000次迭代:

Just convergedPolicy <- Random.runIO $ Pipes.index 5000000 policies
plotPolicy convergedPolicy "policy.svg"

我现在想要在每500,000步骤上绘制中间策略,以了解它们如何收敛 . 我编写了几个函数,它们将 policies 生成器提取出一个列表( [Policy s a] ),例如10个策略 - 每500,000次迭代一次 - 然后绘制所有这些函数 .

然而,这些函数需要更长的时间(10x)并使用更多的内存(4x),而不仅仅是绘制上面的最终策略,即使学习迭代的总数应该相同(即5,000,000) . 我怀疑这是由于提取了一个禁止垃圾收集器的列表,这似乎是Pipes的一个单一用法:

惯用管道样式在生成元素时立即使用元素,而不是将所有元素加载到内存中 .

Producer 超过某个随机monad(即 Random )并且我想要产生的效果是 IO 时,正确使用这种管道的方法是什么?

换句话说,我想将 Producer (Policy s a) Random x 插入 Consumer (Policy s a) IO x .

1 回答

  • 2

    Random 是读取生成器的读取器

    import Control.Monad.Primitive
    import System.Random.MWC
    
    newtype Random a = Random {
        runRandom :: forall m. PrimMonad m => Gen (PrimState m) -> m a
    }
    

    我们可以将 Random a 简单地转换为 ReaderT (Gen (PrimState m)) m a . 这个简单的操作就是 hoistProducer ... Random a 转换为 Producer ... IO a .

    import Control.Monad.Trans.Reader
    
    toReader :: PrimMonad m => Random a -> ReaderT (Gen (PrimState m)) m a
    toReader = ReaderT . runRandom
    

    由于 toReader 是微不足道的,因此它不会产生任何随机的生成开销 . 编写此函数只是为了演示其类型签名 .

    import Pipes
    
    hoistToReader :: PrimMonad m => Proxy a a' b b' Random                          r ->
                                    Proxy a a' b b' (ReaderT (Gen (PrimState m)) m) r
    hoistToReader = hoist toReader
    

    这里有两种方法 . 简单的方法是 hoistConsumer 放入同一个monad,将管道组合在一起,然后运行它们 .

    type ReadGenIO = ReaderT GenIO IO
    
    toReadGenIO :: MFunctor t => t Random a -> t ReadGenIO a
    toReadGenIO = hoist toReader
    
    int :: Random Int
    int = Random uniform
    
    ints :: Producer Int Random x
    ints = forever $ do
        i <- lift int
        yield i
    
    sample :: Show a => Int -> Consumer a IO ()
    sample 0 = return ()
    sample n = do
        x <- await
        lift $ print x
        sample (n-1)
    
    sampleSomeInts :: Effect ReadGenIO ()
    sampleSomeInts = hoist toReader ints >-> hoist lift (sample 1000)
    
    runReadGenE :: Effect ReadGenIO a -> IO a
    runReadGenE = withSystemRandom . runReaderT . runEffect
    
    example :: IO ()
    example = runReadGenE sampleSomeInts
    

    管道用户应该注意 Pipes.Lift 中的另一组工具 . 这些是运行变形金刚的工具,例如 Random monad,它通过 Proxy 分发 . 这里有预先构建的工具,用于运行变压器库中熟悉的变压器 . 它们都是由distribute构建的 . 它将 Proxy ... (t m) a 转换为 t (Proxy ... m) a ,您可以使用用于运行 t 的任何工具运行一次 .

    import Pipes.Lift
    
    runRandomP :: PrimMonad m => Proxy a a' b b' Random r ->
                                 Gen (PrimState m) -> Proxy a a' b b' m r
    runRandomP = runReaderT . distribute . hoist toReader
    

    您可以将管道组合在一起并使用 runEffect 来摆脱 Proxy ,但是当您将 Proxy ... IO r 组合在一起时,您将自己处理生成器参数 .

相关问题