I am trying to write a Spider Solitaire player as a Haskell learning exercise.
My main
function will call a playGame
function once for each game (using mapM
), passing in the game number and a random generator (StdGen
). The playGame
function should return a Control.Monad.State
monad and an IO monad that contains a String
showing the game tableau and a Bool
indicating if the game was won or lost.
How do I combine the State
monad with the IO
monad for the return value? What should the type declaration for `playGame be?
playGame :: Int -> StdGen a -> State IO (String, Bool)
Is the State IO (String, Bool)
correct? If not, what should it be?
In main
, I plan on using
do
-- get the number of games from the command line (already written)
results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]
Is this the correct way to call playGame
?
What you want is StateT s IO (String, Bool)
, where StateT
is provided by both Control.Monad.State
(from the mtl
package) and Control.Monad.Trans.State
(from the transformers
package).
This general phenomenon is called a monad transformer, and you can read a great introduction to them in Monad Transformers, Step by Step.
There are two approaches to defining them. One of them is found in the transformers
package which uses the MonadTrans
class to implement them. The second approach is found in the mtl
class and uses a separate type-class for each monad.
The advantage of the transformers
approach is the use of a single type-class to implement everything (found here):
class MonadTrans t where
lift :: Monad m => m a -> t m a
lift
has two nice properties which any instance of MonadTrans
must satisfy:
(lift .) return = return
(lift .) f >=> (lift .) g = (lift .) (f >=> g)
These are the functor laws in disguise, where (lift .) = fmap
, return = id
and (>=>) = (.)
.
The mtl
type-class approach has its benefits, too, and some things can only be cleanly solved using the mtl
type-classes, however the disadvantage is then that each mtl
type-class has its own set of laws you have to remember when implement instances for it. For example, the MonadError
type-class (found here)is defined as:
class Monad m => MonadError e m | m -> e where
throwError :: e -> m a
catchError :: m a -> (e -> m a) -> m a
This class comes with laws, too:
m `catchError` throwError = m
(throwError e) `catchError` f = f e
(m `catchError` f) `catchError` g = m `catchError` (\e -> f e `catchError` g)
These are just the monad laws in disguise, where throwError = return
and catchError = (>>=)
(and the monad laws are the category laws in disguise, where return = id
and (>=>) = (.)
).
For your specific problem, the way you would write your program would be the same:
do
-- get the number of games from the command line (already written)
results <- mapM (\game -> playGame game getStdGen) [1..numberOfGames]
... but when you write your playGame
function it would look either like:
-- transformers approach :: (Num s) => StateT s IO ()
do x <- get
y <- lift $ someIOAction
put $ x + y
-- mtl approach :: (Num s, MonadState s m, MonadIO m) => m ()
do x <- get
y <- liftIO $ someIOAction
put $ x + y
There are more differences between the approaches that become more apparent when you start stacking more than one monad transformer, but I think that's a good start for now.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With