Search code examples
haskellmonad-transformersstate-monadnewtype

Hiding nested state transformers with newtype in haskell


I'm not sure what I want to accomplish is sane or not (please be nice). But i had an idea for a small game, the game will need to have some state and the state is updated with some random component (otherwise it would be kind of boring). Seeing as the StdGen is also some kind of state i started modelling my program like this

import Control.Monad.State
import qualified System.Random as R
type Rng = StateT StdGen IO

random :: (Random a) => Rng a
random = state R.random

randoms :: (Random a) => Int -> Rng [a]
randoms n = replicateM n random

type GameS = [Int]-- not important right now

type Game = StateT GameS Rng

mainGame :: Game ()
mainGame = do
    s <- gets
    n <- lift $ randoms 10 :: (Rng [Int]) 
    put s ++ n

mainRng :: Rng ()
mainRng = do
    liftIO $ "Inside Rng!"
    -- here do stuff that has to do with game setup and so on dependent on rng
    runStateT mainGame [1,2,3,4]


main :: IO ()
main = do
    g <- R.newStdGen
    runStateT mainRng g

Alright that worked! So let's try and hide some of our details behind a newtype.

-- change type aliases for Game and Rng to newtype
newtype Rng a {
   runR :: StateT R.StdGen IO a
} deriving (Applicative, Functor, Monad, MonadIO, MonadState R.StdGen)

newtype Game a {
   runG :: StateT GameS Rng a
} deriving (Applicative, Functor, Monad, MonadIO, MonadState GameS)

-- and create a expose newRun functions
runRng :: Rng a -> IO (a, R.StdGen)
runRng k = do
    g <- R.newStdGen
    runStateT (runR k) g

runGame :: Game a -> Rng (a, GameS)
runGame k = let initial = [1,2,3]
             in runStateT (runG k) initial

-- mainGame as before
mainGame :: Game ()
mainGame = do
   liftIO $ print "Inside game"
   s <- gets
   n <- lift $ randoms 10 :: (Rng [Int])
   put s ++ n

main :: IO ()
main = do
    final <- runRng $ do
        liftIO $ print "Inside rng moand"
        runGame mainGame
    print $ show final

This works to a point. Inside mainGame i can do liftIO and all the state operations except when i try to lift to get some random numbers i get a error couldn't match type 't0 Rng' with 'Game'

Do i somehow need to implement MonadTrans for my Game and Rng types?

Any help with thid would be great!


Solution

  • I think I figured it out. If i changed mainGame like so:

    mainGame :: Game ()
    mainGame = do
       liftIO $ print "Inside game"
       s <- gets
       n <- Game . lift $ randoms 10 :: (Rng [Int])
       put s ++ n
    

    It works as expected. And looking at the definition of MonadTrans lift it would seem like thats what im missing. So makeing a Game instance of MonadTrans would solve my problem.

    so doing the following changes to the Game type:

    newtype GameT m a {
        runG :: StateT GameS m a
    } deriving (Applicative, Functor, Monad, MonadIO, MonadState, MonadTrans GameS)
    type Game = GameT Rng
    

    will let me do what I want. I'm still no quite sure why i have to make GameT with the signature GameT m a to be able to make an instance of MonadTrans, struggeling with the types here, maybe some one can chimn in on that.