{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} -- | Transient implements an event handling mechanism ("backtracking") which -- allows registration of one or more event handlers to be executed when an -- event occurs. This common underlying mechanism called is used to handle -- three different types of events: -- -- * User initiated actions to run undo and retry actions on failures -- * Finalization actions to run at the end of a task -- * Exception handlers to run when exceptions are raised -- -- Backtracking works seamlessly across thread boundaries. The freedom to put -- the undo, exception handling and finalization code where we want it allows -- us to write modular and composable code. -- -- Note that backtracking (undo, finalization or exception handling) does not -- change or automatically roll back the user defined state in any way. It only -- executes the user installed handlers. State changes are only caused via user -- defined actions. Any state changes done within the backtracking actions are -- accumulated on top of the user state as it was when backtracking started. -- This example prints the final state as "world". -- -- @ -- import Transient.Base (keep, setState, getState) -- import Transient.Backtrack (onUndo, undo) -- import Control.Monad.IO.Class (liftIO) -- -- main = keep $ do -- setState "hello" -- oldState <- getState -- -- liftIO (putStrLn "Register undo") \`onUndo` (do -- curState <- getState -- liftIO $ putStrLn $ "Final state: " ++ curState -- liftIO $ putStrLn $ "Old state: " ++ oldState) -- -- setState "world" >> undo >> return () -- @ -- -- See -- <https://www.fpcomplete.com/user/agocorona/the-hardworking-programmer-ii-practical-backtracking-to-undo-actions this blog post> -- for more details. module Transient.Backtrack ( -- * Multi-track Undo -- $multitrack onBack, back, forward, backCut, -- * Default Track Undo -- $defaulttrack onUndo, undo, retry, undoCut, -- * Finalization Primitives -- $finalization onFinish, onFinish', finish, noFinish, initFinish ) where import Transient.Internals import Data.Typeable import Control.Applicative import Control.Monad.State import Unsafe.Coerce import System.Mem.StableName import Control.Exception import Control.Concurrent.STM hiding (retry) import Data.Maybe -- $defaulttrack -- -- A default undo track with the track id of type @()@ is provided. APIs for -- the default track are simpler as they do not require the track id argument. -- -- @ -- import Control.Concurrent (threadDelay) -- import Control.Monad.IO.Class (liftIO) -- import Transient.Base (keep) -- import Transient.Backtrack (onUndo, undo, retry) -- -- main = keep $ do -- step 1 >> tryAgain >> step 2 >> step 3 >> undo >> return () -- where -- step n = liftIO (putStrLn ("Do Step: " ++ show n)) -- \`onUndo` -- liftIO (putStrLn ("Undo Step: " ++ show n)) -- -- tryAgain = liftIO (putStrLn "Will retry on undo") -- \`onUndo` -- (retry >> liftIO (threadDelay 1000000 >> putStrLn "Retrying...")) -- @ -- $multitrack -- -- Transient allows you to pair an action with an undo action ('onBack'). As -- actions are executed the corresponding undo actions are saved. At any point -- an 'undo' can be triggered which executes all the undo actions registered -- till now in reverse order. At any point, an undo action can decide to resume -- forward execution by using 'forward'. -- -- Multiple independent undo tracks can be defined for different use cases. An -- undo track is identified by a user defined data type. The data type of each -- track must be distinct. -- -- @ -- import Control.Concurrent (threadDelay) -- import Control.Monad.IO.Class (liftIO) -- import Transient.Base (keep) -- import Transient.Backtrack (onBack, forward, back) -- -- data Track = Track String deriving Show -- -- main = keep $ do -- step 1 >> goForward >> step 2 >> step 3 >> back (Track \"Failed") >> return () -- where -- step n = liftIO (putStrLn $ "Execute Step: " ++ show n) -- \`onBack` -- \(Track r) -> liftIO (putStrLn $ show r ++ " Undo Step: " ++ show n) -- -- goForward = liftIO (putStrLn "Turning point") -- \`onBack` \(Track r) -> -- forward (Track r) -- >> (liftIO $ threadDelay 1000000 -- >> putStrLn "Going forward...") -- @ -- $finalization -- -- Several finish handlers can be installed (using 'onFinish') that are called -- when the action is finalized using 'finish'. All the handlers installed -- until the last 'initFinish' are invoked in reverse order; thread boundaries -- do not matter. The following example prints "3" and then "2". -- -- @ -- import Control.Monad.IO.Class (liftIO) -- import Transient.Base (keep) -- import Transient.Backtrack (initFinish, onFinish, finish) -- -- main = keep $ do -- onFinish (\\_ -> liftIO $ putStrLn "1") -- initFinish -- onFinish (\\_ -> liftIO $ putStrLn "2") -- onFinish (\\_ -> liftIO $ putStrLn "3") -- finish Nothing -- return () -- @ -- --data Backtrack b= Show b =>Backtrack{backtracking :: Maybe b -- ,backStack :: [EventF] } -- deriving Typeable -- -- -- ---- | assures that backtracking will not go further back --backCut :: (Typeable reason, Show reason) => reason -> TransientIO () --backCut reason= Transient $ do -- delData $ Backtrack (Just reason) [] -- return $ Just () -- --undoCut :: TransientIO () --undoCut = backCut () -- ---- | the second parameter will be executed when backtracking --{-# NOINLINE onBack #-} --onBack :: (Typeable b, Show b) => TransientIO a -> ( b -> TransientIO a) -> TransientIO a --onBack ac bac= registerBack (typeof bac) $ Transient $ do -- Backtrack mreason _ <- getData `onNothing` backStateOf (typeof bac) -- runTrans $ case mreason of -- Nothing -> ac -- Just reason -> bac reason -- where -- typeof :: (b -> TransIO a) -> b -- typeof = undefined -- --onUndo :: TransientIO a -> TransientIO a -> TransientIO a --onUndo x y= onBack x (\() -> y) -- -- ---- | register an action that will be executed when backtracking --{-# NOINLINE registerUndo #-} --registerBack :: (Typeable b, Show b) => b -> TransientIO a -> TransientIO a --registerBack witness f = Transient $ do -- cont@(EventF _ _ x _ _ _ _ _ _ _ _) <- get -- !!> "backregister" -- -- md <- getData `asTypeOf` (Just <$> backStateOf witness) -- -- case md of -- Just (bss@(Backtrack b (bs@((EventF _ _ x' _ _ _ _ _ _ _ _):_)))) -> -- when (isNothing b) $ do -- addrx <- addr x -- addrx' <- addr x' -- to avoid duplicate backtracking points -- setData $ if addrx == addrx' then bss else Backtrack mwit (cont:bs) -- Nothing -> setData $ Backtrack mwit [cont] -- -- runTrans f -- where -- mwit= Nothing `asTypeOf` (Just witness) -- addr x = liftIO $ return . hashStableName =<< (makeStableName $! x) -- -- --registerUndo :: TransientIO a -> TransientIO a --registerUndo f= registerBack () f -- ---- | restart the flow forward from this point on --forward :: (Typeable b, Show b) => b -> TransIO () --forward reason= Transient $ do -- Backtrack _ stack <- getData `onNothing` (backStateOf reason) -- setData $ Backtrack(Nothing `asTypeOf` Just reason) stack -- return $ Just () -- --retry= forward () -- --noFinish= forward (FinishReason Nothing) -- ---- | execute backtracking. It execute the registered actions in reverse order. ---- ---- If the backtracking flag is changed the flow proceed forward from that point on. ---- ---- If the backtrack stack is finished or undoCut executed, `undo` will stop. --back :: (Typeable b, Show b) => b -> TransientIO a --back reason = Transient $ do -- bs <- getData `onNothing` backStateOf reason -- !!>"GOBACK" -- goBackt bs -- -- where -- -- goBackt (Backtrack _ [] )= return Nothing -- !!> "END" -- goBackt (Backtrack b (stack@(first : bs)) )= do -- (setData $ Backtrack (Just reason) stack) -- -- mr <- runClosure first -- !> "RUNCLOSURE" -- -- Backtrack back _ <- getData `onNothing` backStateOf reason -- -- !> "END RUNCLOSURE" -- case back of -- Nothing -> case mr of -- Nothing -> return empty -- !> "FORWARD END" -- Just x -> runContinuation first x -- !> "FORWARD EXEC" -- justreason -> goBackt $ Backtrack justreason bs -- !> ("BACK AGAIN",back) -- --backStateOf :: (Monad m, Show a, Typeable a) => a -> m (Backtrack a) --backStateOf reason= return $ Backtrack (Nothing `asTypeOf` (Just reason)) [] -- --undo :: TransIO a --undo= back () -- -------- finalization -- --newtype FinishReason= FinishReason (Maybe SomeException) deriving (Typeable, Show) -- ---- | initialize the event variable for finalization. ---- all the following computations in different threads will share it ---- it also isolate this event from other branches that may have his own finish variable --initFinish= backCut (FinishReason Nothing) -- ---- | set a computation to be called when the finish event happens --onFinish :: ((Maybe SomeException) ->TransIO ()) -> TransIO () --onFinish f= onFinish' (return ()) f -- -- ---- | set a computation to be called when the finish event happens this only apply for --onFinish' ::TransIO a ->((Maybe SomeException) ->TransIO a) -> TransIO a --onFinish' proc f= proc `onBack` \(FinishReason reason) -> -- f reason -- -- ---- | trigger the event, so this closes all the resources --finish :: Maybe SomeException -> TransIO a --finish reason= back (FinishReason reason) -- -- ---- | kill all the processes generated by the parameter when finish event occurs --killOnFinish comp= do -- chs <- liftIO $ newTVarIO [] -- onFinish $ const $ liftIO $ killChildren chs -- !> "killOnFinish event" -- r <- comp -- modify $ \ s -> s{children= chs} -- return r -- ---- | trigger finish when the stream of data ends --checkFinalize v= -- case v of -- SDone -> finish Nothing >> stop -- SLast x -> return x -- SError e -> liftIO ( print e) >> finish Nothing >> stop -- SMore x -> return x