monad-schedule-0.2: src/Control/Monad/Schedule/FreeAsync.hs
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
-- | Asynchronicity implementation using 'MVar's and free monads.
module Control.Monad.Schedule.FreeAsync (
-- * 'FreeAsyncT'
FreeAsyncT (..),
FreeAsync,
freeAsync,
asyncMVar,
runFreeAsync,
runFreeAsyncT,
-- * Concurrent 'Applicative' interface
ConcurrentlyT (..),
Concurrently,
concurrently,
concurrentlyMVar,
lift',
runConcurrentlyT,
runConcurrently,
)
where
-- base
import Control.Arrow (second, (>>>))
import Control.Concurrent (MVar, forkIO, newEmptyMVar, putMVar, takeMVar, threadDelay, tryTakeMVar, yield)
import Control.Monad.IO.Class
import Data.Either (partitionEithers)
import Data.List.NonEmpty (NonEmpty (..), appendList, toList)
-- transformers
import Control.Monad.Trans.Class
-- operational
import Control.Monad.Operational (ProgramT, ProgramViewT (..), interpretWithMonadT, singleton, unviewT, viewT)
-- monad-schedule
import Control.Monad.Schedule.Class (MonadSchedule (..), apSchedule)
{- | An 'IO'-like monad with the capability of async/await-style futures.
Synchronous (blocking) computations in this monad can be created using 'lift' and 'liftIO'.
Asynchronous computations that can run in the background are created with 'freeAsync' or 'asyncMVar'.
To leverage the asynchronicity, you can schedule computations with 'MonadSchedule' operations such as 'schedule' or 'race'.
Caution: Composing computations with 'Applicative' or 'Monad' operations like '<*>', '>>=' and @do@-notation
will force all but the final computation in order:
When running @a '*>' b '*>' c@, @b@ will not be started before @a@ has completed.
To start all operations and run them concurrently, use e.g. 'Control.Monad.Schedule.Class.scheduleWith'.
To use an 'Applicative' interface for concurrency, have a look at 'ConcurrentlyT'.
-}
newtype FreeAsyncT m a = FreeAsyncT {getFreeAsyncT :: ProgramT MVar m a}
deriving newtype (Functor, Applicative, Monad, MonadTrans)
type FreeAsync = FreeAsyncT IO
-- FIXME MFunctor & MMonad instances pending https://github.com/HeinrichApfelmus/operational/pull/28/
-- | Lifts into 'FreeAsyncT' /without/ concurrency. See 'freeAsync'.
instance (MonadIO m) => MonadIO (FreeAsyncT m) where
liftIO = lift . liftIO
{- | Run an 'IO' computation in the background.
This returns a "promise", or "future", which completes the computation when run.
-}
freeAsync :: (MonadIO m) => IO a -> FreeAsyncT m a
freeAsync action = FreeAsyncT $ do
var <- liftIO newEmptyMVar
liftIO $ forkIO $ putMVar var =<< action
singleton var
-- | Complete all computations and remove the 'FreeAsyncT' layer.
runFreeAsyncT :: (MonadIO m) => FreeAsyncT m a -> m a
runFreeAsyncT = interpretWithMonadT (liftIO . takeMVar) . getFreeAsyncT
-- | Like 'runFreeAsyncT', but specialized to 'IO'.
runFreeAsync :: FreeAsync a -> IO a
runFreeAsync = runFreeAsyncT
{- | Asynchronously await an 'MVar'.
@'asyncMVar' var@ will attempt 'takeMVar' in a way that can be 'schedule'd concurrently with other 'asyncMVar's or 'freeAsync's.
-}
asyncMVar :: MVar a -> FreeAsyncT m a
asyncMVar = FreeAsyncT . singleton
data MVarCont m a = forall b.
MVarCont
{ mvar :: MVar b
, cont :: b -> ProgramT MVar m a
}
embedMVarCont :: (Monad m) => MVarCont m a -> FreeAsyncT m a
embedMVarCont MVarCont {mvar, cont} = FreeAsyncT $ unviewT $ mvar :>>= cont
{- | Concurrently wait for the completion of 'IO' actions.
Has a slight runtime overhead over the direct @'MonadSchedule' 'IO'@ instance, but better fairness.
-}
instance (MonadIO m) => MonadSchedule (FreeAsyncT m) where
schedule actions = retryForever $ getFreeAsyncT <$> actions
where
retryForever :: (MonadIO m) => NonEmpty (ProgramT MVar m a) -> FreeAsyncT m (NonEmpty a, [FreeAsyncT m a])
retryForever actions = do
-- Look at all actions
views <- lift (mapM viewT actions)
-- Have some of them finished?
case partitionNonEmpty $ viewToEither <$> views of
-- All have finished
Left (as, []) -> return (as, [])
-- Some have finished, some are waiting for MVars
Left (as, cont : conts) -> do
-- Peek at the MVars
progressed <- lift $ tryProgresses $ cont :| conts
-- Are some MVars present already?
case progressed of
-- Yes, some were present, step the corresponding actions
Left (actions, conts) -> do
-- Look at the progressed actions
views <- lift (mapM viewT actions)
-- Have some of them returned now?
case partitionNonEmpty $ viewToEither <$> views of
-- Yes. Return those as well
Left (as', conts') -> return (as <> as', embedMVarCont <$> (conts ++ conts'))
-- No, they are blocked on other MVars now
Right conts' -> return (as, embedMVarCont <$> toList conts' <> conts)
-- All MVars are still blocked
Right conts -> return (as, embedMVarCont <$> toList conts)
-- All actions are waiting for MVars
Right conts -> do
-- Retry until some MVars get unblocked
(progressed, conts) <- lift $ retryProgresses conts
-- Some MVars are unblocked, start over.
retryForever $ appendList progressed $ getFreeAsyncT . embedMVarCont <$> conts
viewToEither :: ProgramViewT MVar m a -> Either a (MVarCont m a)
viewToEither (Return a) = Left a
viewToEither (mvar :>>= cont) = Right MVarCont {mvar, cont}
partitionNonEmpty :: NonEmpty (Either a b) -> Either (NonEmpty a, [b]) (NonEmpty b)
partitionNonEmpty (Left a :| abs) = let (as, bs) = partitionEithers abs in Left (a :| as, bs)
partitionNonEmpty (Right b :| abs) = case partitionEithers abs of
([], bs) -> Right $ b :| bs
(a : as, bs) -> Left (a :| as, b : bs)
tryProgress :: (MonadIO m) => MVarCont m a -> m (Either (ProgramT MVar m a) (MVarCont m a))
tryProgress mvarcont@MVarCont {mvar, cont} = do
result <- liftIO $ tryTakeMVar mvar
return $ maybe (Right mvarcont) (Left . cont) result
tryProgresses :: (MonadIO m) => NonEmpty (MVarCont m a) -> m (Either (NonEmpty (ProgramT MVar m a), [MVarCont m a]) (NonEmpty (MVarCont m a)))
tryProgresses conts = do
result <- partitionNonEmpty <$> mapM tryProgress conts
case result of
Left (progressed, []) -> return $ Left (progressed, [])
Left (progressed, cont : conts) -> do
inner <- tryProgresses $ cont :| conts
case inner of
Left (progressed', finalConts) -> return $ Left (progressed <> progressed', finalConts)
Right finalConts -> return $ Left (progressed, toList finalConts)
Right conts -> return $ Right conts
retryProgresses :: (MonadIO m) => NonEmpty (MVarCont m a) -> m (NonEmpty (ProgramT MVar m a), [MVarCont m a])
retryProgresses conts = do
result <- tryProgresses conts
case result of
Left progress -> return progress
Right _ -> do
liftIO $ yield >> threadDelay 100
retryProgresses conts
{- | Like 'FreeAsyncT', but leverages concurrency in the 'Applicative' interface.
The central difference to 'FreeAsyncT' is the 'Applicative' instance:
@concurrently a *> concurrently b *> concurrently c@ will launch all three actions immediately
and return when all actions have completed.
On the other hand, @concurrently a >>= f@ has to compute sequentially.
For more readable syntax, it can be useful to switch the @ApplicativeDo@ extension on.
The downside of this 'Applicative' instance is that 'ConcurrentlyT' can't be an instance of 'MonadTrans'.
As a drop-in replacement, the function 'lift'' is supplied.
Caution: To lift an 'IO' action concurrently, you need to use 'concurrently' and not 'liftIO'.
-}
newtype ConcurrentlyT m a = ConcurrentlyT {getConcurrentlyT :: FreeAsyncT m a}
deriving newtype (Functor, Monad, MonadIO)
type Concurrently = ConcurrentlyT IO
{- | Lift an 'IO' action such that it can be run concurrently.
See 'freeAsync'.
-}
concurrently :: (MonadIO m) => IO a -> ConcurrentlyT m a
concurrently = ConcurrentlyT . freeAsync
-- | Like 'asyncMVar'
concurrentlyMVar :: MVar a -> ConcurrentlyT m a
concurrentlyMVar = ConcurrentlyT . asyncMVar
{- | Lift a computation to 'ConcurrentlyT'.
This replaces the missing 'MonadTrans' instance.
Caution: Computations lifted with this function cannot be scheduled concurrently!
If this is your intention, 'concurrently' needs to be used instead.
-}
lift' :: (Monad m) => m a -> ConcurrentlyT m a
lift' = ConcurrentlyT . lift
-- | Run a 'ConcurrentlyT' computation to completion, removing the newtype layers.
runConcurrentlyT :: (MonadIO m) => ConcurrentlyT m a -> m a
runConcurrentlyT = runFreeAsyncT . getConcurrentlyT
-- | Like 'runConcurrently', but specialised to 'IO'.
runConcurrently :: Concurrently a -> IO a
runConcurrently = runConcurrentlyT
instance (MonadIO m) => Applicative (ConcurrentlyT m) where
pure = ConcurrentlyT . pure
(<*>) = apSchedule
-- | Like 'FreeAsyncT', but executes actions composed via the 'Applicative' interface concurrently.
instance (MonadIO m) => MonadSchedule (ConcurrentlyT m) where
schedule =
fmap getConcurrentlyT
>>> schedule
>>> fmap (second (map ConcurrentlyT))
>>> ConcurrentlyT