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 ?


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.


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 (eg 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.


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 like Thing -> (Thing, a) I would go "aha, this looks a bit like State , maybe this can be simplified to State Thing a ". Understanding and working with plain functions is an important first step on the road to using State or its friends.
  • IO , on the other hand, is the only thing that can do its job. But the name playGame 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 without IO . 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 a StdGen from System.Random . You just have to make sure you thread your seed (the StdGen ) correctly (doing this automatically was basically why State 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 an IO action, so you need to bind its result with <- in a do -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 each playGame , which may or may not be what you want. If it isn't, well, you could return the seed from each playGame when you were done with it, to pass to the next one, or repeatedly get new seeds with newStdGen (which you could do from inside playGame , if you decide to keep it in IO ).

  • Anyway, this hasn't been a very structured answer, for which I apologise, but I hope it gives you something to think about.

    链接地址: http://www.djcxy.com/p/43358.html

    上一篇: 为什么一个函数不能使用monadic值并返回另一个monadic值?

    下一篇: 在Haskell中合并monad