如何在 IO 中采样 RVarT

How to sample RVarT in IO

我很难在 random-fu 中思考 RVarT。作为一项脑力练习,我正在尝试使用 monad 转换器

随机生成 Maybe x 并将它们组合成 Maybe (x, x)

我设法做到了这一点,这对我来说似乎很直观

maybeRandSome :: (MaybeT RVar) Int
maybeRandSome = lift $ return 1

maybeRandNone :: (MaybeT RVar) Int
maybeRandNone = MaybeT . return $ Nothing

maybeTwoRands :: (MaybeT RVar) (Int, Int)
maybeTwoRands =
  do
    x <- maybeRandSome
    y <- maybeRandNone
    return (x, y)

并且可以在 IO 中对它们进行采样

> sample $ runMaybeT maybeTwoRands
Nothing

但是我不知道是否可以反过来:

reverseMaybeRandSome :: (RVarT Maybe) Int
reverseMaybeRandSome = lift $ Just 1

reverseMaybeRandNone :: (RVarT Maybe) Int
reverseMaybeRandNone = lift Nothing

reverseMaybeTwoRands :: (RVarT Maybe) (Int, Int)
reverseMaybeTwoRands =
  do
    x <- Random.sample reverseMaybeRandSome
    y <- Random.sample reverseMaybeRandNone
    return (x, y)

这需要我以某种方式从 Maybe m 提升到 MonadRandom m,我不知道这是否有意义,或者我是否在做一些不合理的事情。

是的,你几乎在做一些不合理的事情。 MaybeT m a 同构于任何 monad 的 m (Maybe a),包括 m = RVar,所以 MaybeT RVar a 实际上只是一个 RVar (Maybe a),它是一个随机变量的表示Maybe a 中的值。鉴于此,很容易想象对两个 Maybe a 值的随机变量进行采样并以通常的方式将它们组合成一个 Maybe (a,a) 值的随机变量(即,如果其中一个或两个都是 Nothing ,则结果为Nothing,如果分别为Just xJust y,则结果为Just (x,y))。这就是您的第一段代码所做的。

然而,RVarT Maybe a不同。这是一个 a 值( 不是 Maybe a 值)随机变量,可以 使用基础 Maybe monad 的设施 在生成它的值时,前提是它们可以以某种合理的方式提升到最终的单子,其中实现了随机变量的 "randomness"。

要理解这意味着什么,我们必须更详细地了解类型 RVarRVarT

类型 RVar a 表示一个 a 值的随机变量。为了实际将此表示转换为真正的随机值,您必须 运行 它具有:

runRVar :: RandomSource m s => RVar a -> s -> m a

这有点太笼统了,所以想象一下它专门用于:

runRVar :: RVar a -> StdRandom -> IO a

注意这里StdRandom是唯一有效的StdRandom,所以我们总是写runRVar something StdRandom,也可以写成sample something.

有了这个专业化,您应该将 RVar a 视为 使用一组有限的随机化原语 构建随机变量的单子配方 runRVar转换为实现相对于全局随机数生成器的随机化原语的 IO 动作。这种到 IO 操作的转换允许配方生成实际采样的随机值。如果您有兴趣,可以在 Data.Random.Internal.Source.

中找到有限的随机化原语集

类似地,RVarT n a 也是一个 a 值的随机变量(即,使用一组有限的随机化原语构建随机变量的方法)也可以访问 "facilities of another base monad n"。这个配方可以运行在任何可以实现两者随机化原语基础monad[=53]的最终monad中=].在一般情况下,你 运行 它与:

runRVarTWith :: MonadRandom m => 
    (forall t. n t -> m t) -> RVarT n a -> s -> m a

它采用显式提升函数来解释 如何 将基础 monad n 的设施提升到最终 monad m.

如果基础 monad nMaybe,那么它 "facilities" 能够发出错误或计算失败的信号。您可能会使用这些工具来构造以下有点愚蠢的随机变量:

sqrtNormal :: RVarT Maybe Double
sqrtNormal = do
  z <- stdNormalT
  if z < 0
    then lift Nothing   -- use Maybe facilities to signal error
    else return $ sqrt z

请注意,至关重要的是,sqrtNormal 并不代表要生成的 Maybe Double 值随机变量。相反,它表示 Double 值的随机变量,其生成可能会通过基础 Maybe monad 的设施失败。

为了实现这个随机变量(即对其进行采样),我们需要在适当的最终 monad 中 运行 它。最终的 monad 需要支持随机化原语 Maybe monad 中适当提升失败的概念。

IO 工作正常,如果适当的失败概念是 运行时间错误:

liftMaybeToIO :: Maybe a -> IO a
liftMaybeToIO Nothing = error "simulation failed!"
liftMaybeToIO (Just x) = return x

之后:

main1 :: IO ()
main1 = print =<< runRVarTWith liftMaybeToIO sqrtNormal StdRandom

大约一半时间生成正标准高斯的平方根,另一半时间抛出 运行时间错误。

如果你想以纯形式捕获失败(例如 Maybe),那么你需要考虑在适当的 monad 中实现 RVar。单子:

MaybeT IO a

会成功的。它与 IO (Maybe a) 同构,因此它具有可用的 IO 设施(需要实现随机化原语)并且能够通过 returning Nothing 发出失败信号。如果我们写:

main2 :: IO ()
main2 = print =<< runMaybeT act
  where act :: MaybeT IO Double
        act = sampleRVarTWith liftMaybe sqrtNormal

我们会得到一个错误,提示 MonadRandom (MaybeT IO) 没有实例。我们可以创建一个如下:

import Control.Monad.Trans (liftIO)
instance MonadRandom (MaybeT IO) where
  getRandomPrim = liftIO . getRandomPrim

加上适当的提升功能:

liftMaybe :: Maybe a -> MaybeT IO a
liftMaybe = MaybeT . return

之后,main2 将 return Nothing 大约一半的时间和 Just 正高斯的平方根另一半。

完整代码:

{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE FlexibleInstances #-}

import Control.Monad.Trans (liftIO)
import Control.Monad.Trans.Maybe (MaybeT(..))
import Data.Random
import Data.Random.Lift
import Data.Random.Internal.Source

sqrtNormal :: RVarT Maybe Double
sqrtNormal = do
  z <- stdNormalT
  if z < 0
    then lift Nothing   -- use Maybe facilities to signal error
    else return $ sqrt z

liftMaybeToIO :: Maybe a -> IO a
liftMaybeToIO Nothing = error "simulation failed!"
liftMaybeToIO (Just x) = return x

main1 :: IO ()
main1 = print =<< runRVarTWith liftMaybeToIO sqrtNormal StdRandom

instance MonadRandom (MaybeT IO) where
  getRandomPrim = liftIO . getRandomPrim

main2 :: IO ()
main2 = print =<< runMaybeT act
  where act :: MaybeT IO Double
        act = runRVarTWith liftMaybe sqrtNormal StdRandom
        liftMaybe :: Maybe a -> MaybeT IO a
        liftMaybe = MaybeT . return

这将全部应用于您的第二个示例的方式看起来像这样,它将始终打印 Nothing:

{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE FlexibleInstances #-}

import Control.Monad.Trans (liftIO)
import Control.Monad.Trans.Maybe (MaybeT(..))
import Data.Random
import Data.Random.Lift
import Data.Random.Internal.Source

reverseMaybeRandSome :: RVarT Maybe Int
reverseMaybeRandSome = return 1

reverseMaybeRandNone :: RVarT Maybe Int
reverseMaybeRandNone = lift Nothing

reverseMaybeTwoRands :: RVarT Maybe (Int, Int)
reverseMaybeTwoRands =
  do
    x <- reverseMaybeRandSome
    y <- reverseMaybeRandNone
    return (x, y)

instance MonadRandom (MaybeT IO) where
  getRandomPrim = liftIO . getRandomPrim

runRVarTMaybe :: RVarT Maybe a -> IO (Maybe a)
runRVarTMaybe act = runMaybeT $ runRVarTWith liftMaybe act StdRandom
  where
    liftMaybe :: Maybe a -> MaybeT IO a
    liftMaybe = MaybeT . return

main :: IO ()
main = print =<< runRVarTMaybe reverseMaybeTwoRands