我如何通过 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
Random
是 mwc-random
的包装,可以是 ST
或 IO
:
的 运行
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
。这个 琐碎的 操作是您想要 hoist
将 Producer ... 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
摆脱 Proxy
s,但是当您组合 Proxy ... IO r
时您会自己处理生成器参数我们在一起。
我有一个 Producer
使用我自己的 Random
monad 创建依赖于随机性的值:
policies :: Producer (Policy s a) Random x
Random
是 mwc-random
的包装,可以是 ST
或 IO
:
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
:
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
是一个读取生成器
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
。这个 琐碎的 操作是您想要 hoist
将 Producer ... 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
摆脱 Proxy
s,但是当您组合 Proxy ... IO r
时您会自己处理生成器参数我们在一起。