Search code examples
haskellrandommonad-transformers

How to sample RVarT in IO


I am having difficulties wrapping my brain around RVarT in random-fu. Just as a mental exercise I am trying to generate Maybe x randomly and combining them in Maybe (x, x), using monad transformers

I have manged to pull this off, which seems intuitive to me

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)

And can sample them in IO doing this

> sample $ runMaybeT maybeTwoRands
Nothing

However I cannot figure out if the reverse is possible:

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)

Which requires me to lift from Maybe m to MonadRandom m somehow, and I can't figure out if that makes sense or if I am doing something unsound to begin with.


Solution

  • Yes, you're pretty much doing something unsound. MaybeT m a is isomorphic to m (Maybe a) for any monad, including m = RVar, so a MaybeT RVar a is really just an RVar (Maybe a), which is a representation of a random variable taking values in Maybe a. Given this, it's easy enough to imagine sampling two Maybe a-valued random variables and combining them into a Maybe (a,a)-value random variable in the usual manner (i.e., if either or both are Nothing, the result is Nothing, and if they're Just x and Just y respectively, the result is Just (x,y)). That's what your first chunk of code is doing.

    However, RVarT Maybe a is different. It's a a-valued (not Maybe a-valued) random variable that can use the facilities of the base Maybe monad in generating its values, provided they can be lifted in some sensible way to the final monad in which the "randomness" of the random variable is realized.

    To understand what this means, we have to take a more detailed look at the types RVar and RVarT.

    The type RVar a represents an a-valued random variable. In order to actually turn this representation into a real random value, you have to run it with:

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

    This is a little too general, so imagine it being specialized to:

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

    Note that StdRandom is the only valid value of StdRandom here, so we'll always write runRVar something StdRandom, which can also be written sample something.

    With this specialization, you should view an RVar a as a monadic recipe for constructing a random variable using a limited set of randomization primitives that runRVar converts into IO actions that realize the randomization primitives with respect to a global random number generator. This conversion to IO actions is what allows the recipe to generate an actual sampled random value. If you're interested, you can find the limited set of randomization primitives in Data.Random.Internal.Source.

    Similarly, RVarT n a is also an a-valued random variable (i.e., a recipe for constructing a random variable using a limited set of randomization primitives) that also has access to the "facilities of another base monad n". This recipe can be run inside any final monad that can realize both the randomization primitives and the facilities of the base monad n. In the general case, you run it with:

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

    which takes an explicit lifting function that explains how to lift the facilities of the base monad n to the final monad m.

    If the base monad n is Maybe, then it's "facilities" are the ability to signal an error or failed computation. You might use those facilities to construct the following somewhat silly random variable:

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

    Note that, critically, sqrtNormal does not represent a Maybe Double-valued random variable to be generated. Instead it represents Double-valued random variable whose generation can fail via the facilities of the base Maybe monad.

    In order to realize this random variable (i.e., sample it), we need to run it in an appropriate final monad. The final monad needs to support both the randomization primitives and an appropriately lifted notion of failure from the Maybe monad.

    IO works fine, if the appropriate notion of failure is a runtime error:

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

    after which:

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

    generates the square root of a positive standard Gaussian about half the time and throws a runtime error the other half.

    If you want to capture failure in a pure form (as a Maybe, for example), then you need to consider realizing the RVar in an appropriate monad. The monad:

    MaybeT IO a
    

    will do the trick. It's isomorphic to IO (Maybe a), so it has IO facilities available (needed to realize the randomization primitives) and is capable of signaling failure by returning Nothing. If we write:

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

    we'll get an error that there's no instance for MonadRandom (MaybeT IO). We can create one as follows:

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

    together with an appropriate lifting function:

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

    After which, main2 will return Nothing about half the time and Just the square root of a positive Gaussian the other half.

    The full code:

    {-# 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
    

    The way this would all apply to your second example would look something like this, which will always print 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