Maybe (or maybe not) it happened to you at least once during your Haskell journey to look at functions like this…
stackManip :: State Stack Int
= do
stackManip 8
push
pop pop
…and to think: “How can possibly this work?” In this post I’ll desugar the State
monad up to a point where all the other monads will go crazy screaming “Oh my God, it’s naked!”. But let’s start from the foundations.
The aim of this post is not teaching you how to use the State
monad, I assume you are already familiar with it. Let’s first recall:
instance Monad (State s) where
return x = State $ \s -> (x,s)
>>= f = State $ \s ->
m case runState m s of
-> runState (f x) s' (x, s')
This will be useful later on. For demonstration purposed I’m going to use a shameless copy of the stack examples you’ll find in Learn You a Haskell, because it’s easy to grasp and perfect for our purposes. Let’s define a Stack
as a type synonym for a list of Int
, plus to stateful computations to push and pop stuff to/from the stack:
type Stack = [Int]
push :: Int -> State Stack ()
= state $ \xs -> ((), x:xs)
push x
pop :: State Stack Int
= state $ \(x:xs) -> (x, xs) pop
Armed with these two, we can write another stateful computation that modify the stack:
stackManip :: State Stack Int
= do
stackManip 8
push
pop pop
And finally run the example:
main :: IO ()
= print . show $ runState stackManip [1,2,4,9] main
This works. But how? In other terms, when I was a beginner Haskeller, this was the question I kept repeating to myself “Where is the state (the stack) passed around?” From my personal experience this often confuses newcomers, which might expect every function dealing with the state to take in input the “old” state in order to modify it. In other terms, this is what the newcomer expects:
push' :: Stack -> State Stack ()
= undefined --not implemented push'
And the fact that he sees just a stateful computation instead, without any “access point” for the old state to be passed in, confuses him. I’m going to shed light exactly over this.
Let’s get back to our main for a moment:
main :: IO ()
= print . show $ runState stackManip [1,2,4,9] main
In order to yield a result, we need to evaluate stackManip
, so let’s take a look at it, again (repetita iuvant):
stackManip :: State Stack Int
= do
stackManip 8
push
pop pop
We can easily rewrite the function without the do block, this way:
stackManip' :: State Stack Int
= push 8 >> pop >>= \_ -> pop stackManip'
We can now substitute the functions which their actual implementations:
stackManip'' :: State Stack Int
= (state $ \s -> ((), 8:s)) >>
stackManip'' $ \(x:xs) -> (x,xs)) >>= \_ ->
(state $ \(x':xs') -> (x',xs') state
Here I’ve already made a simplification: we already know what the new state for push 8
will be: exactly the same we had at he beginning plus “8” on the top of the stack.
Now it’s time to recall how (>>)
is defined in Haskell:
(>>) :: m a -> m b -> m b
>> m2 = m1 >>= \_ -> m2 m1
Which yield the following:
stackManip''' :: State Stack Int
= ((state $ \s -> ((), 8:s)) >>= \_ ->
stackManip''' $ \(x:xs) -> (x,xs))) >>= \_ ->
(state $ \(x':xs') -> (x',xs') state
If you look carefully, I’ve grouped on purpose the first two stateful computation, to stress the fact we are going to desugar something like this:
>>= op2) >>= op3 (op1
And now the fun begins: we need to desugar even further (>>=)
, with the definition I gave you at the beginning of the article:
stackManip'''' :: State Stack Int
= (state $ \k ->
stackManip'''' case runState m k of
-> runState (f x) s') >>= \_ ->
(x, s') $ \(x':xs') -> (x',xs')
state where
= state $ \s -> ((), 8:s)
m = state $ \(x'':xs'') -> (x'', xs'') f _
The only thing I’ve done here to enforce readability is to move the two stateful computations we want to bind into two separate expressions, giving them a nomenclature similar to the familiar one (m >>= f
). Let’s substitute further, getting rid of the case and of the first runState
. I’m going to do two passages at once, but it’s mechanical if you follow the original implementation of (>>=)
:
sm' :: State Stack Int
= (state $ \k ->
sm' $ \(x:xs) -> (x, xs)) (8:k)) >>= \_ ->
runState (state $ \(x'':xs'') -> (x'', xs'') state
Let’s simplify a bit the expression in the middle: state
and runState
and inverse of each other:
sm'' :: State Stack Int
= (state $ \k -> (8,k)) >>= \_ ->
sm'' $ \(x:xs) -> (x,xs) state
Much leaner! But now, unfortunately we’re going to beef up this once again, to get rid of the second bind. But hang thigh, once we do this we’ll have our final result:
sm''' :: State Stack Int
= state $ \q ->
sm''' case runState (state $ \k -> (8,k)) q of
-> runState (f x) s'
(x,s') where f _ = state $ \(x':xs') -> (x',xs')
It should not come as a surprise what I’ve done here: I’ve just applied again the definition of (>>=)
, and moved the function to bind into a separate expression. It’s not time to simplify everything the way we did before (namely getting rid of the first runState
and case
expression):
sm'''' :: State Stack Int
= state $ \q -> runState (state $ \(x:xs) -> (x,xs)) q sm''''
And finally simplifying that expression we yield our final computation!
smFinal :: State Stack Int
= state $ \(q:qs) -> (q,qs) smFinal
Well, this was somewhat expected, because pushing something and popping twice is equivalent to just pop once from the initial stack! Notice what happened: we started with a handful of “actions”, and we mechanically boiled them down to a single value! It’s now straightforward to see how this can work:
= print . show $ runState smFinal [1,2,4,9] main
All we do is to extract a function from our State
(with runState
) and apply the underlying function to the initial stack. Nothing magical at all!
I would say “hell no”! Is the abstraction which makes Haskell the wonderful language it is, so the compiler already does the “menial” job for you. All you need to do is to write your stateful computations with the idea that whatever you’ll write in a do block will be “sequenced” together and the state will be passed around as “functions applications”.
I hope I was able to convey several messages:
State
is pure, it’s just function application all over the place, in a CPS fashion.