Combining monads in Haskell
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
?
Solution 1:
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.
Solution 2:
State
is a monad, and IO
is a monad. What you're trying to write from scratch is called a "monad transformer", and the Haskell standard library already defines what you need.
Have a look at the state monad transformer StateT
: it has a parameter which is the inner monad you want to wrap into the State
.
Each monad transformer implements a bunch of typeclasses, such that for each instance, the transformer deals with it every time it can (e.g. the state transformer is only able to directly handle state-related functions), or it propagates the call to the inner monad in such a way that when you can stack all the transformers you want, and have a uniform interface to access to the features of all of them. It's a sort of chain of responsibility, if you want to look at it this way.
If you look on hackage, or do a quick search on stack overflow or google you'll find lots of examples of usages of StateT
.
edit: Another interesting reading is Monad Transformers Explained.
Solution 3:
Okay, a few things to clear up here:
- You can't "return a monad". A monad is a kind of type, not a kind of value (to be precise, a monad is a type constructor that has an instance of the
Monad
class). I know this sounds pedantic, but it might help you sort out the distinction between things and types-of-things in your head, which is important. - Note that you can't do anything with
State
that is impossible without it, so if you're confused about how to use it, then don't feel you need to! Often, I just write the ordinary function type I want, and then if I notice I have a lot of functions shaped likeThing -> (Thing, a)
I would go "aha, this looks a bit likeState
, maybe this can be simplified toState Thing a
". Understanding and working with plain functions is an important first step on the road to usingState
or its friends. -
IO
, on the other hand, is the only thing that can do its job. But the nameplayGame
doesn't immediately spring out at me as the name of something that needs to do I/O. In particular, if you only need (pseudo-)random numbers, you can do that withoutIO
. As a commenter has pointed out, MonadRandom is great for making this simple, but again you can just use pure functions that take and returning aStdGen
fromSystem.Random
. You just have to make sure you thread your seed (theStdGen
) correctly (doing this automatically was basically whyState
was invented; you might find you understand it better after trying to program without it!) -
Finally, you're not quite using
getStdGen
correctly. It's anIO
action, so you need to bind its result with<-
in ado
-block before using it (technically, you don't need to, you have lots of options, but that's almost certainly what you want to do). Something like this:do seed <- getStdGen results <- mapM (\game -> playGame game seed) [1..numberOfGames]
Here
playGame :: Integer -> StdGen -> IO (String, Bool)
. Notice, however, that you're passing the same random seed to eachplayGame
, which may or may not be what you want. If it isn't, well, you could return the seed from eachplayGame
when you were done with it, to pass to the next one, or repeatedly get new seeds withnewStdGen
(which you could do from insideplayGame
, if you decide to keep it inIO
).
Anyway, this hasn't been a very structured answer, for which I apologise, but I hope it gives you something to think about.