aivika-2.1: Simulation/Aivika/Internal/Cont.hs
-- |
-- Module : Simulation.Aivika.Internal.Cont
-- Copyright : Copyright (c) 2009-2014, David Sorokin <david.sorokin@gmail.com>
-- License : BSD3
-- Maintainer : David Sorokin <david.sorokin@gmail.com>
-- Stability : experimental
-- Tested with: GHC 7.8.3
--
-- The 'Cont' monad is a variation of the standard Cont monad
-- and F# async workflow, where the result of applying
-- the continuations is the 'Event' computation.
--
module Simulation.Aivika.Internal.Cont
(ContCancellation(..),
ContCancellationSource,
Cont(..),
ContParams,
newContCancellationSource,
contCancellationInitiated,
contCancellationInitiate,
contCancellationInitiating,
contCancellationBind,
contCancellationConnect,
invokeCont,
runCont,
rerunCont,
spawnCont,
contParallel,
contParallel_,
catchCont,
finallyCont,
throwCont,
resumeCont,
resumeECont,
contCanceled,
contFreeze,
contAwait) where
import Data.IORef
import Data.Array
import Data.Array.IO.Safe
import Data.Monoid
import Control.Exception
import Control.Monad
import Control.Monad.Trans
import Control.Applicative
import Simulation.Aivika.Internal.Specs
import Simulation.Aivika.Internal.Parameter
import Simulation.Aivika.Internal.Simulation
import Simulation.Aivika.Internal.Dynamics
import Simulation.Aivika.Internal.Event
import Simulation.Aivika.Internal.Signal
-- | It defines how the parent and child computations should be cancelled.
data ContCancellation = CancelTogether
-- ^ Cancel the both computations together.
| CancelChildAfterParent
-- ^ Cancel the child if its parent is cancelled.
| CancelParentAfterChild
-- ^ Cancel the parent if its child is cancelled.
| CancelInIsolation
-- ^ Cancel the computations in isolation.
-- | It manages the cancellation process.
data ContCancellationSource =
ContCancellationSource { contCancellationInitiatedRef :: IORef Bool,
contCancellationActivatedRef :: IORef Bool,
contCancellationInitiatingSource :: SignalSource ()
}
-- | Create the cancellation source.
newContCancellationSource :: Simulation ContCancellationSource
newContCancellationSource =
Simulation $ \r ->
do r1 <- newIORef False
r2 <- newIORef False
s <- invokeSimulation r newSignalSource
return ContCancellationSource { contCancellationInitiatedRef = r1,
contCancellationActivatedRef = r2,
contCancellationInitiatingSource = s
}
-- | Signal when the cancellation is intiating.
contCancellationInitiating :: ContCancellationSource -> Signal ()
contCancellationInitiating =
publishSignal . contCancellationInitiatingSource
-- | Whether the cancellation was initiated.
contCancellationInitiated :: ContCancellationSource -> Event Bool
contCancellationInitiated x =
Event $ \p -> readIORef (contCancellationInitiatedRef x)
-- | Whether the cancellation was activated.
contCancellationActivated :: ContCancellationSource -> IO Bool
contCancellationActivated =
readIORef . contCancellationActivatedRef
-- | Deactivate the cancellation.
contCancellationDeactivate :: ContCancellationSource -> IO ()
contCancellationDeactivate x =
writeIORef (contCancellationActivatedRef x) False
-- | If the main computation is cancelled then all the nested ones will be cancelled too.
contCancellationBind :: ContCancellationSource -> [ContCancellationSource] -> Event DisposableEvent
contCancellationBind x ys =
Event $ \p ->
do hs1 <- forM ys $ \y ->
invokeEvent p $
handleSignal (contCancellationInitiating x) $ \_ ->
contCancellationInitiate y
hs2 <- forM ys $ \y ->
invokeEvent p $
handleSignal (contCancellationInitiating y) $ \_ ->
contCancellationInitiate x
return $ mconcat hs1 <> mconcat hs2
-- | Connect the parent computation to the child one.
contCancellationConnect :: ContCancellationSource
-- ^ the parent
-> ContCancellation
-- ^ how to connect
-> ContCancellationSource
-- ^ the child
-> Event DisposableEvent
-- ^ computation of the disposable handler
contCancellationConnect parent cancellation child =
Event $ \p ->
do let m1 =
handleSignal (contCancellationInitiating parent) $ \_ ->
contCancellationInitiate child
m2 =
handleSignal (contCancellationInitiating child) $ \_ ->
contCancellationInitiate parent
h1 <-
case cancellation of
CancelTogether -> invokeEvent p m1
CancelChildAfterParent -> invokeEvent p m1
CancelParentAfterChild -> return mempty
CancelInIsolation -> return mempty
h2 <-
case cancellation of
CancelTogether -> invokeEvent p m2
CancelChildAfterParent -> return mempty
CancelParentAfterChild -> invokeEvent p m2
CancelInIsolation -> return mempty
return $ h1 <> h2
-- | Initiate the cancellation.
contCancellationInitiate :: ContCancellationSource -> Event ()
contCancellationInitiate x =
Event $ \p ->
do f <- readIORef (contCancellationInitiatedRef x)
unless f $
do writeIORef (contCancellationInitiatedRef x) True
writeIORef (contCancellationActivatedRef x) True
invokeEvent p $ triggerSignal (contCancellationInitiatingSource x) ()
-- | The 'Cont' type is similar to the standard Cont monad
-- and F# async workflow but only the result of applying
-- the continuations return the 'Event' computation.
newtype Cont a = Cont (ContParams a -> Event ())
-- | The continuation parameters.
data ContParams a =
ContParams { contCont :: a -> Event (),
contAux :: ContParamsAux }
-- | The auxiliary continuation parameters.
data ContParamsAux =
ContParamsAux { contECont :: SomeException -> Event (),
contCCont :: () -> Event (),
contCancelSource :: ContCancellationSource,
contCancelFlag :: IO Bool,
contCatchFlag :: Bool }
instance Monad Cont where
return = returnC
m >>= k = bindC m k
instance ParameterLift Cont where
liftParameter = liftPC
instance SimulationLift Cont where
liftSimulation = liftSC
instance DynamicsLift Cont where
liftDynamics = liftDC
instance EventLift Cont where
liftEvent = liftEC
instance Functor Cont where
fmap = liftM
instance Applicative Cont where
pure = return
(<*>) = ap
instance MonadIO Cont where
liftIO = liftIOC
-- | Invoke the computation.
invokeCont :: ContParams a -> Cont a -> Event ()
{-# INLINE invokeCont #-}
invokeCont p (Cont m) = m p
-- | Cancel the computation.
cancelCont :: Point -> ContParams a -> IO ()
{-# NOINLINE cancelCont #-}
cancelCont p c =
do contCancellationDeactivate (contCancelSource $ contAux c)
invokeEvent p $ (contCCont $ contAux c) ()
returnC :: a -> Cont a
{-# INLINE returnC #-}
returnC a =
Cont $ \c ->
Event $ \p ->
do z <- contCanceled c
if z
then cancelCont p c
else invokeEvent p $ contCont c a
bindC :: Cont a -> (a -> Cont b) -> Cont b
{-# INLINE bindC #-}
bindC (Cont m) k =
Cont $ \c ->
Event $ \p ->
do z <- contCanceled c
if z
then cancelCont p c
else invokeEvent p $ m $
let cont a = invokeCont c (k a)
in c { contCont = cont }
-- | Like @return a >>= k@.
callCont :: (a -> Cont b) -> a -> ContParams b -> Event ()
callCont k a c =
Event $ \p ->
do z <- contCanceled c
if z
then cancelCont p c
else invokeEvent p $ invokeCont c (k a)
-- | Exception handling within 'Cont' computations.
catchCont :: Exception e => Cont a -> (e -> Cont a) -> Cont a
catchCont (Cont m) h =
Cont $ \c0 ->
Event $ \p ->
do let c = c0 { contAux = (contAux c0) { contCatchFlag = True } }
z <- contCanceled c
if z
then cancelCont p c
else invokeEvent p $ m $
let econt e0 =
case fromException e0 of
Just e -> callCont h e c
Nothing -> (contECont . contAux $ c) e0
in c { contAux = (contAux c) { contECont = econt } }
-- | A computation with finalization part.
finallyCont :: Cont a -> Cont b -> Cont a
finallyCont (Cont m) (Cont m') =
Cont $ \c0 ->
Event $ \p ->
do let c = c0 { contAux = (contAux c0) { contCatchFlag = True } }
z <- contCanceled c
if z
then cancelCont p c
else invokeEvent p $ m $
let cont a =
Event $ \p ->
invokeEvent p $ m' $
let cont b = contCont c a
in c { contCont = cont }
econt e =
Event $ \p ->
invokeEvent p $ m' $
let cont b = (contECont . contAux $ c) e
in c { contCont = cont }
ccont () =
Event $ \p ->
invokeEvent p $ m' $
let cont b = (contCCont . contAux $ c) ()
econt e = (contCCont . contAux $ c) ()
in c { contCont = cont,
contAux = (contAux c) { contECont = econt } }
in c { contCont = cont,
contAux = (contAux c) { contECont = econt,
contCCont = ccont } }
-- | Throw the exception with the further exception handling.
--
-- By some reason, an exception raised with help of the standard 'throw' function
-- is not handled properly within 'Cont' computation, altough it will be still handled
-- if it will be wrapped in the 'IO' monad. Therefore, you should use specialised
-- functions like the stated one that use the 'throw' function but within the 'IO' computation,
-- which allows already handling the exception.
throwCont :: IOException -> Cont a
throwCont = liftIO . throw
-- | Run the 'Cont' computation with the specified cancelation source
-- and flag indicating whether to catch exceptions from the beginning.
runCont :: Cont a
-- ^ the computation to run
-> (a -> Event ())
-- ^ the main branch
-> (SomeException -> Event ())
-- ^ the branch for handing exceptions
-> (() -> Event ())
-- ^ the branch for cancellation
-> ContCancellationSource
-- ^ the cancellation source
-> Bool
-- ^ whether to support the exception handling from the beginning
-> Event ()
runCont (Cont m) cont econt ccont cancelSource catchFlag =
m ContParams { contCont = cont,
contAux =
ContParamsAux { contECont = econt,
contCCont = ccont,
contCancelSource = cancelSource,
contCancelFlag = contCancellationActivated cancelSource,
contCatchFlag = catchFlag } }
-- | Lift the 'Parameter' computation.
liftPC :: Parameter a -> Cont a
liftPC (Parameter m) =
Cont $ \c ->
Event $ \p ->
if contCatchFlag . contAux $ c
then liftIOWithCatch (m $ pointRun p) p c
else liftIOWithoutCatch (m $ pointRun p) p c
-- | Lift the 'Simulation' computation.
liftSC :: Simulation a -> Cont a
liftSC (Simulation m) =
Cont $ \c ->
Event $ \p ->
if contCatchFlag . contAux $ c
then liftIOWithCatch (m $ pointRun p) p c
else liftIOWithoutCatch (m $ pointRun p) p c
-- | Lift the 'Dynamics' computation.
liftDC :: Dynamics a -> Cont a
liftDC (Dynamics m) =
Cont $ \c ->
Event $ \p ->
if contCatchFlag . contAux $ c
then liftIOWithCatch (m p) p c
else liftIOWithoutCatch (m p) p c
-- | Lift the 'Event' computation.
liftEC :: Event a -> Cont a
liftEC (Event m) =
Cont $ \c ->
Event $ \p ->
if contCatchFlag . contAux $ c
then liftIOWithCatch (m p) p c
else liftIOWithoutCatch (m p) p c
-- | Lift the IO computation.
liftIOC :: IO a -> Cont a
liftIOC m =
Cont $ \c ->
Event $ \p ->
if contCatchFlag . contAux $ c
then liftIOWithCatch m p c
else liftIOWithoutCatch m p c
liftIOWithoutCatch :: IO a -> Point -> ContParams a -> IO ()
{-# INLINE liftIOWithoutCatch #-}
liftIOWithoutCatch m p c =
do z <- contCanceled c
if z
then cancelCont p c
else do a <- m
invokeEvent p $ contCont c a
liftIOWithCatch :: IO a -> Point -> ContParams a -> IO ()
{-# NOINLINE liftIOWithCatch #-}
liftIOWithCatch m p c =
do z <- contCanceled c
if z
then cancelCont p c
else do aref <- newIORef undefined
eref <- newIORef Nothing
catch (m >>= writeIORef aref)
(writeIORef eref . Just)
e <- readIORef eref
case e of
Nothing ->
do a <- readIORef aref
-- tail recursive
invokeEvent p $ contCont c a
Just e ->
-- tail recursive
invokeEvent p $ (contECont . contAux) c e
-- | Resume the computation by the specified parameters.
resumeCont :: ContParams a -> a -> Event ()
{-# INLINE resumeCont #-}
resumeCont c a =
Event $ \p ->
do z <- contCanceled c
if z
then cancelCont p c
else invokeEvent p $ contCont c a
-- | Resume the exception handling by the specified parameters.
resumeECont :: ContParams a -> SomeException -> Event ()
{-# INLINE resumeECont #-}
resumeECont c e =
Event $ \p ->
do z <- contCanceled c
if z
then cancelCont p c
else invokeEvent p $ (contECont $ contAux c) e
-- | Test whether the computation is canceled.
contCanceled :: ContParams a -> IO Bool
{-# INLINE contCanceled #-}
contCanceled c = contCancelFlag $ contAux c
-- | Execute the specified computations in parallel within
-- the current computation and return their results. The cancellation
-- of any of the nested computations affects the current computation.
-- The exception raised in any of the nested computations is propogated
-- to the current computation as well (if the exception handling is
-- supported).
--
-- Here word @parallel@ literally means that the computations are
-- actually executed on a single operating system thread but
-- they are processed simultaneously by the event queue.
contParallel :: [(Cont a, ContCancellationSource)]
-- ^ the list of:
-- the nested computation,
-- the cancellation source
-> Cont [a]
contParallel xs =
Cont $ \c ->
Event $ \p ->
do let n = length xs
worker =
do results <- newArray_ (1, n) :: IO (IOArray Int a)
counter <- newIORef 0
catchRef <- newIORef Nothing
hs <- invokeEvent p $
contCancellationBind (contCancelSource $ contAux c) $
map snd xs
let propagate =
Event $ \p ->
do n' <- readIORef counter
when (n' == n) $
do invokeEvent p $ disposeEvent hs -- unbind the cancellation sources
f1 <- contCanceled c
f2 <- readIORef catchRef
case (f1, f2) of
(False, Nothing) ->
do rs <- getElems results
invokeEvent p $ resumeCont c rs
(False, Just e) ->
invokeEvent p $ resumeECont c e
(True, _) ->
cancelCont p c
cont i a =
Event $ \p ->
do modifyIORef counter (+ 1)
writeArray results i a
invokeEvent p propagate
econt e =
Event $ \p ->
do modifyIORef counter (+ 1)
r <- readIORef catchRef
case r of
Nothing -> writeIORef catchRef $ Just e
Just e' -> return () -- ignore the next error
invokeEvent p propagate
ccont e =
Event $ \p ->
do modifyIORef counter (+ 1)
-- the main computation was automatically canceled
invokeEvent p propagate
forM_ (zip [1..n] xs) $ \(i, (x, cancelSource)) ->
invokeEvent p $
runCont x (cont i) econt ccont cancelSource (contCatchFlag $ contAux c)
z <- contCanceled c
if z
then cancelCont p c
else if n == 0
then invokeEvent p $ contCont c []
else worker
-- | A partial case of 'contParallel' when we are not interested in
-- the results but we are interested in the actions to be peformed by
-- the nested computations.
contParallel_ :: [(Cont a, ContCancellationSource)]
-- ^ the list of:
-- the nested computation,
-- the cancellation source
-> Cont ()
contParallel_ xs =
Cont $ \c ->
Event $ \p ->
do let n = length xs
worker =
do counter <- newIORef 0
catchRef <- newIORef Nothing
hs <- invokeEvent p $
contCancellationBind (contCancelSource $ contAux c) $
map snd xs
let propagate =
Event $ \p ->
do n' <- readIORef counter
when (n' == n) $
do invokeEvent p $ disposeEvent hs -- unbind the cancellation sources
f1 <- contCanceled c
f2 <- readIORef catchRef
case (f1, f2) of
(False, Nothing) ->
invokeEvent p $ resumeCont c ()
(False, Just e) ->
invokeEvent p $ resumeECont c e
(True, _) ->
cancelCont p c
cont i a =
Event $ \p ->
do modifyIORef counter (+ 1)
-- ignore the result
invokeEvent p propagate
econt e =
Event $ \p ->
do modifyIORef counter (+ 1)
r <- readIORef catchRef
case r of
Nothing -> writeIORef catchRef $ Just e
Just e' -> return () -- ignore the next error
invokeEvent p propagate
ccont e =
Event $ \p ->
do modifyIORef counter (+ 1)
-- the main computation was automatically canceled
invokeEvent p propagate
forM_ (zip [1..n] xs) $ \(i, (x, cancelSource)) ->
invokeEvent p $
runCont x (cont i) econt ccont cancelSource (contCatchFlag $ contAux c)
z <- contCanceled c
if z
then cancelCont p c
else if n == 0
then invokeEvent p $ contCont c ()
else worker
-- | Rerun the 'Cont' computation with the specified cancellation source.
rerunCont :: Cont a -> ContCancellationSource -> Cont a
rerunCont x cancelSource =
Cont $ \c ->
Event $ \p ->
do let worker =
do hs <- invokeEvent p $
contCancellationBind (contCancelSource $ contAux c) [cancelSource]
let cont a =
Event $ \p ->
do invokeEvent p $ disposeEvent hs -- unbind the cancellation source
invokeEvent p $ resumeCont c a
econt e =
Event $ \p ->
do invokeEvent p $ disposeEvent hs -- unbind the cancellation source
invokeEvent p $ resumeECont c e
ccont e =
Event $ \p ->
do invokeEvent p $ disposeEvent hs -- unbind the cancellation source
cancelCont p c
invokeEvent p $
runCont x cont econt ccont cancelSource (contCatchFlag $ contAux c)
z <- contCanceled c
if z
then cancelCont p c
else worker
-- | Run the 'Cont' computation in parallel but connect the cancellation sources.
spawnCont :: ContCancellation -> Cont () -> ContCancellationSource -> Cont ()
spawnCont cancellation x cancelSource =
Cont $ \c ->
Event $ \p ->
do let worker =
do hs <- invokeEvent p $
contCancellationConnect
(contCancelSource $ contAux c) cancellation cancelSource
let cont a =
Event $ \p ->
do invokeEvent p $ disposeEvent hs -- unbind the cancellation source
-- do nothing and it will finish the computation
econt e =
Event $ \p ->
do invokeEvent p $ disposeEvent hs -- unbind the cancellation source
invokeEvent p $ throwEvent e -- this is all we can do
ccont e =
Event $ \p ->
do invokeEvent p $ disposeEvent hs -- unbind the cancellation source
-- do nothing and it will finish the computation
invokeEvent p $
enqueueEvent (pointTime p) $
runCont x cont econt ccont cancelSource False
invokeEvent p $
resumeCont c ()
z <- contCanceled c
if z
then cancelCont p c
else worker
-- | Freeze the computation parameters temporarily.
contFreeze :: ContParams a -> Event (Event (Maybe (ContParams a)))
contFreeze c =
Event $ \p ->
do rh <- newIORef Nothing
rc <- newIORef $ Just c
h <- invokeEvent p $
handleSignal (contCancellationInitiating $
contCancelSource $
contAux c) $ \a ->
Event $ \p ->
do h <- readIORef rh
case h of
Nothing ->
error "The handler was lost: contFreeze."
Just h ->
do invokeEvent p $ disposeEvent h
c <- readIORef rc
case c of
Nothing -> return ()
Just c ->
do writeIORef rc Nothing
invokeEvent p $
enqueueEvent (pointTime p) $
Event $ \p ->
do z <- contCanceled c
when z $ cancelCont p c
writeIORef rh (Just h)
return $
Event $ \p ->
do invokeEvent p $ disposeEvent h
c <- readIORef rc
writeIORef rc Nothing
return c
-- | Await the signal.
contAwait :: Signal a -> Cont a
contAwait signal =
Cont $ \c ->
Event $ \p ->
do c <- invokeEvent p $ contFreeze c
r <- newIORef Nothing
h <- invokeEvent p $
handleSignal signal $
\a -> Event $
\p -> do x <- readIORef r
case x of
Nothing ->
error "The signal was lost: contAwait."
Just x ->
do invokeEvent p $ disposeEvent x
c <- invokeEvent p c
case c of
Nothing -> return ()
Just c ->
invokeEvent p $ resumeCont c a
writeIORef r $ Just h