4

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.

asked Oct 18, 2017 at 5:51

1 Answer 1

8

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)
answered Oct 18, 2017 at 6:32
Sign up to request clarification or add additional context in comments.

1 Comment

Wow this is great, thanks. I'll work through it properly tonight. I should have linked to the current Github source actually... I'll do that now.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.