Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 247
Ralph Avatar asked Jun 06 '12 12:06

Ralph


1 Answers

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.

like image 81
Gabriella Gonzalez Avatar answered Oct 18 '22 17:10

Gabriella Gonzalez