Here’s a somewhat informal answer, but hopefully useful. getCC'
returns a continuation to the current point of execution; you can think of it as saving a stack frame. The continuation returned by getCC'
has not only ContT
‘s state at the point of the call, but also the state of any monad above ContT
on the stack. When you restore that state by calling the continuation, all of the monads built above ContT
return to their state at the point of the getCC'
call.
In the first example you use type APP= WriterT [String] (ContT () IO)
, with IO
as the base monad, then ContT
, and finally WriterT
. So when you call loop
, the state of the writer is unwound to what it was at the getCC'
call because the writer is above ContT
on the monad stack. When you switch ContT
and WriterT
, now the continuation only unwinds the ContT
monad because it’s higher than the writer.
ContT
isn’t the only monad transformer that can cause issues like this. Here’s an example of a similar situation with ErrorT
func :: Int -> WriterT [String] (ErrorT String IO) Int
func x = do
liftIO $ print "start loop"
tell [show x]
if x < 4 then func (x+1)
else throwError "aborted..."
*Main> runErrorT $ runWriterT $ func 0
"start loop"
"start loop"
"start loop"
"start loop"
"start loop"
Left "aborted..."
Even though the writer monad was being told values, they’re all discarded when the inner ErrorT
monad is run. But if we switch the order of the transformers:
switch :: Int -> ErrorT String (WriterT [String] IO) ()
switch x = do
liftIO $ print "start loop"
tell [show x]
if x < 4 then switch (x+1)
else throwError "aborted..."
*Main> runWriterT $ runErrorT $ switch 0
"start loop"
"start loop"
"start loop"
"start loop"
"start loop"
(Left "aborted...",["0","1","2","3","4"])
Here the internal state of the writer monad is preserved, because it’s lower than ErrorT
on the monad stack. The big difference between ErrorT
and ContT
is that ErrorT
‘s type makes it clear that any partial computations will be discarded if an error is thrown.
It’s definitely simpler to reason about ContT
when it’s at the top of the stack, but it is on occasion useful to be able to unwind a monad to a known point. A type of transaction could be implemented in this manner, for example.