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

谁说我不能喝 提交于 2019-11-28 03:23:44

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.

I spent some time tracing this in the λ calculus. It generated pages and pages of derivations that I won't attempt to reproduce here, but I did gain a little insight on how the monad stack works. Your type expands as follows:

type APP a = WriterT [String] (ContT () IO) a
           = ContT () IO (a,[String])
           = ((a,[String]) -> IO()) -> IO()

You can similarly expand out Writer's return, >>=, and tell, along with Cont's return, >>=, and callCC. Tracing it is extremely tedious though.

The effect of calling loop in the driver is to abandon the normal continuation and instead return, again, from the call to getCC'. That abandoned continuation contained the code that would have added the current x to the list. So instead, we repeat the loop, but now x is the next number, and only when we hit the last number (and thus don't abandon the continuation) do we piece together the list from ["The quick brown fox"] and ["4"].

Just as “Real World Haskell” emphasizes that the IO monad needs to stay on the bottom of the stack, it also seems important that the continuation monad stays on top.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!