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

How can I idiomatically and efficiently consume a Pipe in some non-IO monad, with an IO action?

我有一个 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 的一种不合常理的使用:

Idiomatic pipes style consumes the elements immediately as they are generated instead of loading all elements into memory.

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

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

Random 是一个读取生成器

的reader
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 是微不足道的,因此 hoist 不会产生任何随机生成开销。编写此函数只是为了演示其类型签名。

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

这里有两种方法。简单的方法是 hoist 你的 Consumer 进入同一个 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 中的另一组工具。这些是 运行ning 转换器的工具,就像你的 Random monad 一样,通过将它分布在 Proxy 上。这里有用于 运行 变形金刚库中熟悉的变形金刚的预构建工具。它们都是由 distribute 构建的。它将 Proxy ... (t m) a 变成 t (Proxy ... m) a,你可以 运行 一次 使用你使用的任何工具 运行 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 摆脱 Proxys,但是当您组合 Proxy ... IO r 时您会自己处理生成器参数我们在一起。