[Haskell-beginners] Counting sheep with the State monad

Brandon S Allbery KF8NH allbery at ece.cmu.edu
Sun Oct 3 01:22:05 EDT 2010


-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 10/3/10 00:20 , Amy de Buitléir wrote:
> I've written a very simple piece of code in order to help me
> understand Monads better:
> 
> ----------
> import "mtl" Control.Monad.State
> 
> countAnotherSheep :: Int -> (String, Int)
> countAnotherSheep n = (show n ++ " sheep", n+1)
> 
> sheepCounter = State { runState = countAnotherSheep }
> ----------
> 
> So far so good. Now I can do things like:
> 
> ghci> evalState sheepCounter 1
> "1 sheep"
> ghci> evalState sheepCounter 2
> "2 sheep"
> 
> ghci> execState sheepCounter 1
> 2
> ghci> execState sheepCounter 2
> 3
> 
> ghci> evalState (sequence $ replicate 5 sheepCounter) 1
> ["1 sheep","2 sheep","3 sheep","4 sheep","5 sheep"]
> 
> But what I really want to do is create a main function in which I:
> 
> 1. Count some sheep.
> 2. Do something else.
> 3. Count some more sheep, picking up where I left off.
> 
> I could thread the state like this:
> 
> increment (_, n) = countAnotherSheep n
> 
> main = do
>   let c = countAnotherSheep 1
>   putStrLn $ fst c
>   let c2 = increment c
>   putStrLn $ fst c2
>   putStrLn "Now I'll do some other stuff"
>   let c3 = increment c2
>   putStrLn $ fst c3
> 
> ... but shouldn't the State monad make threading the state
> unnecessary? It seems like there should be a way to do this, but I
> can't figure it out. Thank you in advance for any suggestions.

You're overthinking it.  The State monad is defined in terms of those
operations precisely so it can hide/abstract them.  The way you actually use
it is:

> countSheepAndStuff :: State Int ()
> countSheepAndStuff = do
>   countAnotherSheep 1
>   doSomethingElsePossiblyCountingMoreSheep
>   countAnotherSheep 1
>   -- countAnotherSheep 1 >> doSomethingElse... >> countAnotherSheep 1
>
> countAnotherSheep :: Int -> State Int ()
> countAnotherSheep howmany = modify (+ howmany)
> -- or:  countAnotherSheep = modify . (+)
>
> doSomethingElsePossiblyCountingMoreSheep :: State Int ()
> doSomethingElsePossiblyCountingMoreSheep = undefined -- you tell me :)
>
> main = do
>   -- this throws out the () and keeps the final state/sheep count
>   let sheep = execState countSheepAndStuff 0
>   putStrLn $ show sheep ++ " sheep total"

You never construct State values manually; while runState looks like a
record accessor (and in fact is), it's actually used to send something
through a State monad without ever actually creating an explicit State.  The
trick here is that the accessor runState's type is a function (s -> (a,s));
if you pass a function of the appropriate type, you are implicitly creating
a State record, and if you then use the methods and the monad machinery (do
or (>>=)/(>>)) you never need to create or touch an actual State value anywhere.

> runState (modify (+1) >> modify (+3)) 0

The type of the parameter to runState here is State Integer () (which is
really Integer -> ((),Integer)), and then we pass an Integer to that
function to kick things off.  The Integers come from defaulting based on the
0, 1, and 3, and the () from the definition of the modify function (modify
:: s -> State s (), with s set to Integer by aforementioned defaulting).
The result is the tuple ((),4).  Since you don't need or care about the (),
you probably would use execState instead of runState in practice.

For the countAnotherSheep you used in your example, constructing lists of
Strings, you may want to consider another monad:  Writer.  When you get to
that point, you can explore how to do it with a monad transformer WriterT
[String] (State Int) () or the reverse transformer StateT Int (Writer
[String]) (), and the situations under which you would want to prefer one or
the other.

- -- 
brandon s. allbery     [linux,solaris,freebsd,perl]      allbery at kf8nh.com
system administrator  [openafs,heimdal,too many hats]  allbery at ece.cmu.edu
electrical and computer engineering, carnegie mellon university      KF8NH
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.10 (Darwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkyoEv0ACgkQIn7hlCsL25XorQCghdnWCqRpToRLMB6rnCtZX/nX
lAoAnRlVQihEBsH8NRLWzaJUxUf3vZz4
=pqwT
-----END PGP SIGNATURE-----


More information about the Beginners mailing list