I was trying to build a very simple example of a coroutine in Haskell using callCC.
My understanding of how callCC :: ((a -> m b) -> m a) -> m a works is that by writing
example :: Cont Int Int
example = do
callCC $ \k -> ...
f
I essentially bind the continutation of that callCC, i.e. f to k. Then, if k is used inside of callCC's body, e.g. callCC $ \k -> ... k 42 ... it will result in the continuation being called with this value, i.e., f 42, otherwise, if k is unused, callCC $ \k -> ... return 41, it will result in f applied to 41 instead. By storing this k in some data structure I gain the ability of resuming the function (example) right after the callCC (because f is bound to k).
The code I have for a coroutine is as follows
{-#LANGUAGE GeneralisedNewtypeDeriving #-}
module Cont where
import Control.Monad.Cont
import Control.Monad.IO.Class(liftIO)
import Control.Monad.State(put, get, runStateT, evalStateT, StateT, MonadState, MonadIO, execStateT)
newtype CoroutineT r m a = CoroutineT {runCoroutineT :: ContT r (StateT [CoroutineT r m ()] m) a}
deriving (Functor,Applicative,Monad,MonadCont,MonadIO, MonadState [CoroutineT r m ()])
schedule :: CoroutineT () IO ()
schedule = do
cs <- get
case cs of
(c:cs) -> do
put cs
c
schedule
[] -> return ()
yield :: CoroutineT () IO ()
yield = callCC $ \k1 -> do
cs <- get
put (cs ++ [k1 ()])
schedule
test1 :: CoroutineT () IO ()
test1 = do
liftIO $ print "HelloTest1"
yield
liftIO $ print "HelloTest1_2"
test2 :: CoroutineT () IO ()
test2 = do
liftIO $ print "HelloTest2"
yield
liftIO $ print "HelloTest2_2"
test_cont :: IO ()
test_cont = do
runStateT (evalContT (runCoroutineT schedule)) [test1,test2]
return ()
where yield stores the continuation in the state which is then resumed by schedule.
I would expect each continuation to be executed twice --- once in schedule after 'invoking' c and then after each yeild gets evaluated. And therefore the expected output should be
"HelloTest1"
"HelloTest2"
"HelloTest1_2"
"HelloTest2_2"
"HelloTest2_2"
"HelloTest1_2"
What happens in reality though is
"HelloTest1"
"HelloTest2"
"HelloTest1_2"
"HelloTest2_2"
"HelloTest1_2"
What might be wrong with my understanding and/or the code?
UPD:
As correctly noted by @Li-yaoXia I was missing the fact that the continuation after invoking k bound inside of callCC is ignored. It can be seen from the definition of a callCC
callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a
callCC f = Cont $ \k1 -> let a2b = \x -> Cont $ \_ -> k1 x in
runCont $ (f a2b) k1
when I invoke the callCC as callCC $ \k -> ... the k is bound to a2b and once I invoke this k outside the callCC it becomes \x -> Cont $ \_ -> k1 x where k1 is the continuation after the callCC and _ is the continuation after the invocation of k which is ignored.
You're probably missing the fact that when a continuation captured with callCC is called (c in schedule), the current continuation at the call site (schedule and whatever follows) is discarded.
PROGRAM | STATE
---------------------------
schedule [test1,test2]
---------------------------
test1 [test2]
schedule
--------------------------- Print "HelloTest1"
yield [test2]
print "HelloTest1_2"
schedule
--------------------------- -- let K1 = do print "HelloTest1_2"; schedule
schedule [test2, K1]
print "HelloTest1_2"
schedule
---------------------------
test2 [K1]
print "HelloTest1_2"
schedule
--------------------------- Print "HelloTest2"
yield [K1]
print "HelloTest2_2"
schedule
print "HelloTest1_2"
schedule
--------------------------- -- let K2 = print "HelloTest2_2"; schedule; print "HelloTest1_2"; schedule
schedule [K1, K2]
print "HelloTest2_2"
schedule
print "HelloTest1_2"
schedule
--------------------------- -- call K1: the current continuation is discarded, the whole program becomes K1 (Note the definition of K1 above is a lie since it doesn't include this detail.)
print "HelloTest1_2" [K2]
schedule
--------------------------- Print "HelloTest1_2"
schedule [K2]
--------------------------- -- call K2: the current continuation is discarded, the whole program becomes K2
print "HelloTest2_2" []
schedule
print "HelloTest1_2"
schedule
--------------------------- Print "HelloTest2_2" and "HelloTest1_2"
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With