Another literate Haskell post:
I've tried a few times to read various documents on the web about
Monad Transformers in Haskell. I think that in almost every case the
authors are trying to show how clever they are rather than explaining
their use. If I had just seen the simplest possible examples of Monad
Transformers at work I would have been able to figure out what was going
on and that would have given me enough information to bootstrap myself
into Monad Transforming enlightenment.
So to save other people the trouble I went through I'm providing you
with examples of Monad Transformers at work. I'm not even going to
explain in detail how they work, they're close to self-evident once the
main features are pointed out.
> import Control.Monad.State
> import Control.Monad.Identity
Firstly here are two examples of the use of the State Monad. This
code isn't intended as a tutorial on the State Monad so I won't
explain how they work.
> test1 = do
> a <- get
> modify (+1)
> b <- get
> return (a,b)
> test2 = do
> a <- get
> modify (++"1")
> b <- get
> return (a,b)
> go1 = evalState test1 0
> go2 = evalState test2 "0"
Note how evalState 'unwraps' the State Monad and gives you back the
answer at the end.
So the question is: how do I combine both of these states into one
so that I can update or read either the integer or the string state
at will? I could cheat and make a new state of type (Int,String)
but that isn't a general solution.
The idea is that you use a Monad Transformer. A Monad Transformer
if like a layer of onion peel. You start with the Identity monad
and then use Monad Transformers to add layers of functionality.
So to get a two-state monad you take the Identity and add two
layers of stateness to it. To get the answer at the end you need
to unwrap the State layers and then unwrap the Identity layer too.
When you're inside your 'do' expression you need a way to choose
which Monad you're talking to. Ordinary Monad commands will
talk to the outermost layer and you can use 'lift' to send your
commands inwards by one layer.
The following should now be self-evident:
> test3 = do
> modify (+ 1)
> lift $ modify (++ "1")
> a <- get
> b <- lift get
> return (a,b)
> go3 = runIdentity $ evalStateT (evalStateT test3 0) "0"
Note that we use evalStateT to unwrap the State layer instead
of evalState. evalStateT is what you use to unwrap one layer
of the Monad. evalState is what you use when your entire Monad
is just the State monad. When State is a layer, rather than
the whole thing, it's called StateT. (Try :type go3 in ghci.)
What if you want to combine IO and State? For various reasons
the IO monad must form the innermost core of your onion so
there's no need to wrap an IO layer around the Identity,
you can just start with IO. And when you unwrap you don't
need to unwrap the IO layer because the Haskell compiler does
that for you. (Now you can see why you can't layer IO, you're
not supposed to unwrap IO as it's the job of the code that
calls main at the top level to do that.)
So here we go:
> test5 = do
> modify (+ 1)
> a <- get
> lift (print a)
> modify (+ 1)
> b <- get
> lift (print b)
> go5 = evalStateT test5 0
At this point you might be suspicious that IO is being handled
slightly differently from State. So here's a StateT layer
wrapped around State:
> test7 = do
> modify (+ 1)
> lift $ modify (++ "1")
> a <- get
> b <- lift get
> return (a,b)
> go7 = evalState (evalStateT test7 0) "0"
You can probably safely ignore my comments and crib the code above
directly for your own use.
And credit where credit's due: I found the following link to be
more helpful than any other in trying to figure this stuff out
http://www.cs.nott.ac.uk/~nhn/MGS2006/LectureNotes/lecture03-9up.pdf
(But I still felt that this link was obfuscating.)
You may now be wondering where test4 and test6 are. So am I.
Update: I just figured out that you don't always need to use the
'lift' operation. Some of the Monad Transformers have been defined
so that the commands will automatically be passed into the inner
Monad (and further) if needed. I think it's slightly confusing to
write code this way, however.
15 Comments:
Pete,
I was going to post this very example on the Haskell Wiki but I had trouble figuring out how to do this. I'm happy for you to do it. If you want, add a link back to here.
If I remember I'll write an example that doesn't use 'lift' and post it here. Otherwise search for "Looking for basic state transformer example" on USENET where I first posted an example. (The title of the USENET post is wrong, I meant to say "monad" but said "state" by accident.)
Thanks. Great post.
Thanks. I found this quite useful.
I STILL can't think of one real-world reason why I'd want to use one. I just don't get it (yet).
So, if I wanted two kinds of state, one readable and one readable and writable, I could wrap a StateT in a ReaderT? Or vice versa?
Tim,
Exactly. Except that if you just have two types then one can be an ordinary monad, and the other can be a monad transformer. Or you can use three levels where both the reader and the writer are monad transformers and they are applied to the identity monad. (I do the former in this post.)
Very helpful post, thanks!
One thing - did you mean to say "Try :type test3 in ghci." rather than :type go3 ?
The latter just yields:
go3 :: (Integer, [Char])
This post is really great. Thanks!
Hmm, do you really need to wrap this around Identity? Can't you just stack StateT on top of State to just get sort of a DoubleState monad?
chessguy,
It's up to you. If you're bothered by the lack of symmetry from having a StateT and a State then you can use the Identitiy monad.
For those wondering why you would want to use this: as an example, I'm trying to implement Ruby Quiz 19 (Yahtzee). How am I supposed to set up an interactive loop with the user and save the current score and random state (for die rolls) throughout the loop? You can't do IO () stuff like putStrLn to show dice rolls while using State (Score,StdGen) x because your return type is no longer IO (). But with something like the test5 example, it's now entirely possible. A terrible workaround would've been to use IO () to read/write the state to a file... Now I can StateT (Score,StdGen) IO () and all is solved.
Yeah, I'm going to have to agree with sigfpe... it's really just up to you how you want to do it. Sometimes multiple solutions stink!
Can not agree any more with your comments on those egghead's explanation on Monad Transformers. Thanks a lot. Really what I want to know.
Can not agree more with your comments on those fake tutorials. This is just what I want to know. After using that for few weeks, I will begin to understand what Transformer really is.
Here's another nice and thorough and typeset :-) tutorial, posted shortly after sigfpe's original:
http://www.grabmueller.de/martin/www/pub/Transformers.en.html
Post a Comment
<< Home