Goto in Haskell: Can anyone explain this seemingly insane effect of continuation monad usage?

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.

Leave a Comment

tech