Как я могу идиотически и эффективно потреблять трубку в какой-то монаде, отличной от IO, с действием IO?

У меня есть 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 дает лучшие и лучшие политики из простого алгоритма обучения подкрепления.

Я могу эффективно построить политику после, скажем, 5 000 000 итераций, индексируя в 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:

Стиль идиоматических труб потребляет элементы немедленно, поскольку они генерируются вместо того, чтобы загружать все элементы в память.

Какой правильный подход к потреблению такой трубы, когда Producer находится над некоторой случайной монадой (т.е. Random), и эффект, который я хочу создать, находится в IO?

Другими словами, я хочу подключить Producer (Policy s a) Random x к Consumer (Policy s a) IO x.

Ответ 1

Random - это считывающее устройство, которое считывает генератор

import Control.Monad.Primitive
import System.Random.MWC

newtype Random a = Random {
    runRandom :: forall m. PrimMonad m => Gen (PrimState m) -> m a
}

Мы можем тривиально преобразовать a Random a в ReaderT (Gen (PrimState m)) m a. Эта тривиальная операция - это то, что вы хотите hoist, чтобы превратить a 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 не будет никаких накладных расходов из 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 в ту же монаду, скомпоновать трубы и запускать их.

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, распределяя его по 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 s, но вы сами жуете аргумент генератора, объединяя Proxy ... IO r вместе.