Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MonadRandom, State and monad transformers

I'm writing some code (around card-playing strategies) that uses State and recursion together. Perhaps this part doesn't need to actually (it already feels clumsy to me, even as a relative beginner), but there are other parts that probably do so my general question stands...

My initial naive implementation is entirely deterministic (the choice of bid is simply the first option provided by the function validBids):

bidOnRound :: (DealerRules d) => d -> NumCards -> State ([Player], PlayerBids) ()
bidOnRound dealerRules cardsThisRound = do
  (players, bidsSoFar) <- get
  unless (List.null players) $ do
     let options = validBids dealerRules cardsThisRound bidsSoFar
     let newBid = List.head $ Set.toList options
     let p : ps = players
     put (ps, bidsSoFar ++ [(p, newBid)])
     bidOnRound dealerRules cardsThisRound

And I call it from:

playGame :: (DealerRules d, ScorerRules s) => d -> s -> StateT Results IO ()
  ...
let (_, bidResults) = execState (bidOnRound dealerRules cardsThisRound) (NonEmpty.toList players, [])

Now I'm aware that I need to bring randomness into this and several other parts of the code. Not wanting to litter IO everywhere, nor pass round random seeds manually all the time, I feel I should be using MonadRandom or something. A library I'm using uses it to good effect. Is this a wise choice?

Here's what I tried:

bidOnRound :: (DealerRules d, RandomGen g) => d -> NumCards -> RandT g (State ([Player], PlayerBids)) ()
bidOnRound dealerRules cardsThisRound = do
  (players, bidsSoFar) <- get
  unless (List.null players) $ do
     let options = Set.toList $ validBids dealerRules cardsThisRound bidsSoFar
     rnd <- getRandomR (0 :: Int, len options - 1)
     let newBid = options List.!! rnd
     let p : ps = players
     put (ps, bidsSoFar ++ [(p, newBid)])
     bidOnRound dealerRules cardsThisRound

but I'm uncomfortable already, plus can't work out how to call this, e.g. using evalRand in combination with execState etc. The more I read on MonadRandom, RandGen and mtl vs others, the less sure I am of what I'm doing...

How should I neatly combine Randomness and State and how do I call these properly?

Thanks!

EDIT: for reference, full current source on Github.

like image 660
declension Avatar asked Jan 30 '23 09:01

declension


1 Answers

Well how about an example to help you out. Since you didn't post a full working code snippet I'll just replace a lot of your operations and show how the monads can be evaluated:

import Control.Monad.Trans.State
import Control.Monad.Random
import System.Random.TF

bidOnRound :: (RandomGen g) => Int -> RandT g (State ([Int], Int)) ()
bidOnRound i =
 do rand <- getRandomR (10,20)
    s <- lift $ get
    lift $ put ([], i + rand + snd s)

main :: IO ()
main =
 do g <- newTFGen
    print $ flip execState ([],1000) $ evalRandT (bidOnRound 100) g

The thing to note here is you "unwrap" the outer monad first. So if you have RandT (StateT Reader ...) ... then you run RandT (ex via evalRandT or similar) then the state then the reader. Secondly, you must lift from the outer monad to use operations on the inner monad. This might seem clumsy and that is because it is horribly clumsy.

The best developers I know - those whose code I enjoy looking at and working with - extract monad operations and provide an API with all the primitives complete so I don't need to think about the structure of the monad while I'm thinking about the structure of the logic I'm writing.

In this case (it will be slightly contrived since I wrote the above without any application domain, rhyme or reason) you could write:

type MyMonad a = RandT TFGen (State ([Int],Int)) a

runMyMonad :: MyMonad () -> IO Int
runMyMonad f =
 do g <- newTFGen
    pure $ snd $ flip execState ([],1000) $ evalRandT f g

With the Monad defined as a simple alias and execution operation the basic functions are easier:

flipCoin ::  MyMonad Int
flipCoin = getRandomR (10,20)

getBaseValue :: MyMonad Int
getBaseValue = snd <$> lift get

setBaseValue :: Int -> MyMonad ()
setBaseValue v = lift $ state $ \s -> ((),(fst s, v))

With that leg-work out of the way, which is usually a minor part of making a real application, the domain specific logic is easier to write and certainly easier to read:

bidOnRound2 :: Int -> MyMonad ()
bidOnRound2 i =
 do rand <- flipCoin
    old  <- getBaseValue
    setBaseValue (i + rand + old)

main2 :: IO ()
main2 = print =<< runMyMonad (bidOnRound2 100)
like image 106
Thomas M. DuBuisson Avatar answered Feb 08 '23 13:02

Thomas M. DuBuisson