effectful-0.0.0.0: src/Effectful/Coroutine.hs
-- | Experimental support for coroutines.
module Effectful.Coroutine
( Coroutine
, Status(..)
, runCoroutine
, yield
) where
import Control.Concurrent.MVar
import Control.Exception
import Data.Function
import qualified Control.Concurrent as C
import Effectful.Internal.Env
import Effectful.Internal.Has
import Effectful.Internal.Monad
data Coroutine o i = forall es r. Coroutine
{ crInput :: MVar (i, Env es)
, crState :: MVar (State o r)
, crCallerEnvSize :: Int
}
data Status es o i r
= Done r
| Yielded o (i -> Eff es (Status es o i r))
runCoroutine :: Eff (Coroutine o i : es) r -> Eff es (Status es o i r)
runCoroutine (Eff m) = impureEff $ \es -> do
size <- sizeEnv es
mvInput <- newEmptyMVar
mvState <- newEmptyMVar
mask $ \restore -> do
-- Create a worker thread and continue execution there.
tid <- C.forkIO $ do
let cr = Coroutine mvInput mvState size
er <- try $ restore . m =<< unsafeConsEnv cr es
tryPutMVar mvState (either Failure Success er) >>= \case
False -> error "unexpected"
True -> pure ()
waitForStatus restore es size tid mvInput mvState
yield :: Coroutine o i :> es => o -> Eff es i
yield o = impureEff $ \es -> mask $ \restore -> do
Coroutine{..} <- getEnv es
size <- sizeEnv es
-- Save local part of the environment as the caller will discard it.
localEs <- takeLastEnv (size - crCallerEnvSize) es
-- Pass control to the caller.
tryPutMVar crState (Yield o) >>= \case
False -> error "unexpected"
True -> do
(i, callerEs) <- restore $ takeMVar crInput
-- The caller resumed, reconstruct the local environment. The environment
-- needs to be replaced since the one we just got might be completely
-- different to what we had before suspending the computation, e.g. if the
-- computation was resumed in a different thread.
unsafeReplaceEnv es =<< unsafeAppendEnv callerEs localEs
pure i
----------------------------------------
-- Internal
data State o r where
Failure :: SomeException -> State o r
Success :: r -> State o r
Yield :: o -> State o r
waitForStatus
:: (forall a. IO a -> IO a)
-> Env es
-> Int
-> C.ThreadId
-> MVar (i, Env es)
-> MVar (State o r)
-> IO (Status es o i r)
waitForStatus restore0 es0 size0 tid mvInput mvState = fix $ \loop -> do
try @SomeException (restore0 $ takeMVar mvState) >>= \case
Left e -> throwTo tid e >> loop
Right (Failure e) -> throwIO e
Right (Success r) -> Done r <$ unsafeTrimEnv size0 es0
Right (Yield o) -> Yielded o k <$ unsafeTrimEnv size0 es0
where
k i = impureEff $ \es -> mask $ \restore -> do
size <- sizeEnv es
-- Resume suspended computation with the current environment.
tryPutMVar mvInput (i, es) >>= \case
False -> error "unexpected"
True -> waitForStatus restore es size tid mvInput mvState