Takehome
This task involves taking an existing coroutine system and extending it with a new kind of coroutine that functions like an iteratee allowing you to write processes that take in data. It begins with some boilerplate, and then implements the pausable coroutine and the generator coroutine. The below code only uses the base and mtl packages and is compilable on GHC 8.6.5. Please implement the iteratee functions.
import Data.Functor.Identity (Identity (..))
import Control.Monad (liftM, ap)
import Control.Monad.Trans (MonadTrans (..))
{-|
Coroutine s m r | where
s : suspension functor type
m : runtime monad type
r : result type
= Coroutine Record {
execute :: m Continuation | where
m : runtime monad type containing a Continuation
}
execute :: Coroutine s m r -> m Continuation
execute :: Coroutine s m r -> m (Running (s (Coroutine s m r)) | Result (r))
One could see that a Coroutine record simply denotes a variable number of nested/wrapped Continuation states.
The execute function simply unwraps the Coroutine to it's runtime monad containing the first level of continuation state.
A coroutine is a recursive data structure, it recursively stores continuation states.
It is possible to also use a pure coroutine without the monad. But a monad is more general as you can use a pure monad like
Identity monad.
-}
data Coroutine s m r = Coroutine {
execute :: m (Continuation s m r)
}
{-|
A continuation is a reification of program state.
Program state can either be Running or Done.
Running contains a functor of Coroutines.
Which means executing a Coroutine could lead us to 1 layer of action, a tagged container called `Running` which contains a suspension functor which contextualises the next Coroutine wrapper which wraps a subsequent Continuation.
Done contains the final result.
A complete representation might be:
Coroutine (
m1 (
Running (
s1 (
Coroutine (
m2 (
Running (
s2 (
Coroutine (
m3 (
Done r1
))))))))))
A coroutine contains a runtime monad containing a continuation containing a suspension/result.
A suspension contains a coroutine.
-}
data Continuation s m r = Running (s (Coroutine s m r))
| Done (r)
{-|
We recognise that `Coroutine s m` is a monad that contains a result `r`.
This means a `Coroutine s m` can be sequenced with subsequent actions that produce `Coroutine s m`.
What does it mean to sequence a Coroutine? It certainly doesn't mean to execute a Coroutine, as monad sequencing operations return us a monad. They build up a monad.
This tells us that the pure return operation gives us the most simple Coroutine monad, one that contains a continuation which simply denotes a single result.
And that the bind operation is matter of binding functions that produce coroutines to prior coroutines. Since we have understood that a coroutine denotes a variable number of nested continuation states. Binding is the way in which produce this nested configuration of continuation states.
We should be able to see that by nesting Running inside Running, we get multiple "onion" layers of continuation states to be unwrapped by the execution functions.
We should see that if the nth continuation state is a Running state. Then binding a function producing a Coroutine, should bind to nth+1 internal coroutine of `Running (s (Coroutine s m r))`.
If the nth-level continuation state is a Done state. Then binding a function producing a Coroutine, should just apply the function to the Done result, giving us back a new Coroutine.
What this should mean, is that:
If a Running1 binds to a function producing a Running2, then states are just nested like Running1 (Running2).
If a Running1 binds to a function producing a Done1, then states are just nested like Running1 (Done1).
If a Done1 binds to a function producing a Running1, then states is left as Running1 parameterised by Done1.
If a Done1 binds to a function producing a Done2, then the states is left as Done2 parameterised by Done1.
Meaning that in the end, no matter what, there will always only be 1 Done in a Continuation (at the most deepest nesting level), but possibly many Running layers separated by suspension functors.
-}
instance (Functor s, Monad m) => Functor (Coroutine s m) where
fmap = liftM
instance (Functor s, Monad m) => Applicative (Coroutine s m) where
pure = return
(<*>) = ap
instance (Functor s, Monad m) => Monad (Coroutine s m) where
-- | Creates a base Coroutine that contains only a finished continuation
return r = Coroutine (return (Done r))
-- | Sequences a coroutine with a function producing coroutines. In effect a bind, which means a map and reduce.
co >>= f =
Coroutine (
execute co >>= -- ^ The action monad is bound...
\continuation -> case continuation of
Running s -> return $ Running $ fmap (>>= f) s -- ^ Suspension of the next coroutine gets fmapped by `>>= f`, the result is another Coroutine, wrapped up into a Running continuation, and wrapped back into an action monad.
Done r -> execute $ f r -- ^ Just apply the f to the result. Unwrap the coroutine, to get the internal action monad.
)
{-|
We recognise that `Coroutine s` is a monad transformer, as it a type constructor that contains monads `m r`.
Meaning that we can lift arbitrary monads in the `Coroutine s` monad.
We lift an effect, by liftM Done making it work on Monads `e`, then the result is a monad of a continuation.
See how Done :: r -> Continuation s m r.
Then liftM Done :: m r -> m Continuation s m r
The result is then contained inside Coroutine.
-}
instance (Functor s) => MonadTrans (Coroutine s) where
lift e = Coroutine $ liftM Done e -- ^ Lifting arbitrary effects always lifts them into a finished continuation Done. Makes sense right? When the effect is finished, why would the continuation still be running?
-- | Realise that all of our "construction" functions always construct a finished continuation Done. So how do we get a Running continuation? Easy. We know a Running continuation is just something that contains a suspended continuation. So all we need is something that takes a suspension, and gives us back a coroutine containing that suspension.
suspend :: (Functor s, Monad m) => s (Coroutine s m r) -> Coroutine s m r
suspend s = Coroutine $ return (Running s)
type Pause m r = Coroutine Identity m r -- ^ Suspension is Identity co
type Generator a m r = Coroutine ((,) a) m r -- ^ Suspension is (a, co)
pause :: (Monad m) => Pause m ()
pause = suspend (Identity $ return ())
yield :: (Monad m) => a -> Generator a m ()
yield a = suspend (a, return ())
pausableProcess :: Pause IO ()
pausableProcess = do
lift (putStrLn "Hello...")
pause
lift (putStrLn "World...")
return ()
generatingProcess :: Generator String IO String
generatingProcess = do
lift (putStrLn "I'mma firing ma lazer...")
yield "BOOOOOOM!"
lift (putStrLn "Here are some apples: ")
yield "apples!"
return "...meh"
executePausable :: IO ()
executePausable = do
Running (Identity co) <- execute pausableProcess
Done result <- execute $ co
return result
executeGenerator :: IO ()
executeGenerator = do
Running ((value, co)) <- execute generatingProcess
print value
Running ((value, co)) <- execute co
print value
Done result <- execute co
print result
Using the above as a guide, add in these additional types and functions:
type Iteratee b m r = ... -- ^ Suspension is a -> co
await :: (Monad m) => Iteratee b m b
await = ...
iterateeProcess :: (Show b) => Iteratee b IO String
iterateeProcess = do
...
executeIteratee :: IO ()
executeIteratee = do
...