3
votes

I'm having some trouble figuring out /how/ the bind operator would actually bind together the following State monads:

pop :: State [Int] Int
pop = do
        (x:xs) <- get
        put xs
        return x

push :: Int -> State [Int] ()
push x = do 
            xs <- get
            put (x:xs)

doStuff :: State [Int] ()
doStuff = do
            pop
            x <- pop
            push 5
            push x

Take doStuff, which can be desugared to the following:

pop >>= (\_ -> pop >>= (\x -> push 5 >>= (\_ -> push x)))

When this line is evaluated, in what order does the binding actually happen? Since, to actually bind, Haskell needs to get a State monad out of the function on the right of the >>= operator (i.e. the function right operands need to be fully evaluated first), I would've thought that the following would happen:

  1. s1 = push 5 >>= (\_ -> push x)
  2. s2 = pop >>= (\x -> s1)
  3. s3 = pop >>= (\_ -> s2)

Is this the right way to think about it? I feel that I understand monads well, but my biggest problem is in actually visualising what's happening "behind the scenes" and how the data is flowing, so to speak. The do notation gives the illusion that I'm dealing with a bunch of sequential operations, when in fact, there's a whole bunch of nesting and closures.

I feel somewhat like I'm over-thinking things here and further confusing myself as a result.

3
You should expand the definition of >>= from the state monad to get a feel for what happens. There's nothing special going on, just ordinary evaluation.augustss
The evaluation strategy just doesn't matter here, so there's no right way to think about it. You can easily see that by trying different ways of evaluation. The result will always be the same.Niklas B.

3 Answers

8
votes

Starting from

pop >>= (\_ -> pop >>= (\x -> push 5 >>= (\_ -> push x)))

a few functions can be inlined (to show what is happening better). I will start with (>>=), pretending that State is not defined as a transformer or newtype, to keep things simple.

type State s a = s -> (a, s)
m >>= k = \ s -> let (a, s') = m s in k a s'

\ s -> let (a, s') = pop s in
(\ _ -> pop >>= (\ x -> push 5 >>= (\ _ -> push x))) a s'

\ s -> let (_, s') = pop s in
(pop >>= (\ x -> push 5 >>= (\ _ -> push x))) s'

\ s -> let (_, s') = pop s in
let (a, s'') = pop s' in
(\ x -> push 5 >>= (\ _ -> push x)) a s''

\ s -> let (_, s') = pop s in
let (a, s'') = pop s' in
(push 5 >>= (\ _ -> push a)) s''

\ s -> let (_, s') = pop s in
let (a, s'') = pop s' in
let (b, s''') = push 5 s'' in
(\ _ -> push a)) b s'''


\ s -> let (_, s') = pop s in
let (a, s'') = pop s' in
let (_, s''') = push 5 s'' in
push a s'''
3
votes

Is this the right way to think about it?

No.

First of all: while it is obviously correct to think of "first this happens, than that, then we evaluate this keyboard input..." in the IO monad, this isn't true for all monads. For instance, in the list monad this doesn't really make any sense. In general, it's not possible to assign a particular order to calculations in Haskell at all, it's not defined behaviour.

Yet, it is always possible, and very often helpful, to think of an order of computations in a monad, and this order is in fact the one suggested by the do notation. So, most of the time it isn't actually insightful to think about the desugared expression. But if you wish to make that step, here's how I'd do it:

  1. pop >>= \_ -> THUNK1

  2. THUNK1 ≡> pop >>= \x -> THUNK2

  3. {Closure{x}} THUNK2 ≡> push 5 >>= \_ -> THUNK3

  4. {Closure{x}} THUNK3 ≡> push x

Which is of course grotesquely more ugly, but says pretty much the same as the sugared do expression.

2
votes

When this line is evaluated, in what order does the binding actually happen?

There's nothing special about "binding" here. The desugared expression is evaluated in exactly the same (lazy) way any other expression would be, and the specifics depend on the implementation of (>>=) for the particular monad you're working with.

If we're talking about using something like runState, given an expression like foo >>= (\x -> bar), the outermost expression is the application of (>>=) but we're trying to unwrap a newtype and then apply the function inside, so the (>>=) gets forced, as does the function.

If we consider instead the list monad, (>>=) is concatMap. Given an expression like [foo1, foo2] >>= (\x -> [bar1, bar2, x] >>= (\y -> [baz, y])), using take 5 on the result is clearly not going to fully compute all the binds.


That said, there is one important rule here: To whatever extent evaluating x >>= f forces the evaluation of x, in a large expression like a desugared do block the forcing will occur in the apparent "sequential" order, for the same reason that the "sequential illusion" is possible.