packages feed

pipes-safe (empty) → 1.0.0

raw patch · 5 files changed

+1132/−0 lines, 5 filesdep +basedep +pipesdep +transformerssetup-changed

Dependencies added: base, pipes, transformers

Files

+ Control/Proxy/Safe.hs view
@@ -0,0 +1,577 @@+-- | Exception handling and resource management integrated with proxies++{-# LANGUAGE Rank2Types, CPP, KindSignatures #-}++module Control.Proxy.Safe (+    -- * Exception Handling+    -- $exceptionp+    module Control.Proxy.Trans.Either,+    module Exception,+    ExceptionP,+    throw,+    catch,+    handle,++    -- * Safe IO+    SafeIO,+    runSafeIO,+    runSaferIO,+    trySafeIO,+    trySaferIO,++    -- * Checked Exceptions+    -- $check+    CheckP(..),+    tryK,+    tryIO,++    -- * Finalization+    onAbort,+    finally,+    bracket,+    bracket_,+    bracketOnAbort,++    -- * Prompt Finalization+    -- $prompt+    unsafeCloseU,+    unsafeCloseD,+    unsafeClose+    ) where++import qualified Control.Exception as Ex+import Control.Exception as Exception (SomeException, Exception)+import Control.Applicative (Applicative(pure, (<*>)))+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Reader (ReaderT(ReaderT, runReaderT), asks)+import qualified Control.Proxy as P+import qualified Control.Proxy.Core.Fast as PF+import qualified Control.Proxy.Core.Correct as PC+import Control.Proxy ((>->))+import Control.Proxy.Trans.Identity+import qualified Control.Proxy.Trans.Either as E+import qualified Control.Proxy.Trans.Reader as R+import Control.Proxy.Trans.Either hiding (throw, catch, handle)+import Data.IORef (IORef, newIORef, readIORef, writeIORef)+#if MIN_VERSION_base(4,6,0)+#else+import Prelude hiding (catch)+#endif++{- $exceptionp+    This library checks and stores all exceptions using the 'EitherP' proxy+    transformer.  The following type synonym simplifies type signatures.++    Use 'runEitherP' / 'runEitherK' from the re-exported+    @Control.Proxy.Trans.Either@ to convert 'ExceptionP' back to the base+    monad++    This module does not re-export 'E.throw', 'E.catch', and 'E.handle' from+    @Control.Proxy.Trans.Either@ and instead defines new versions similar to the+    API from @Control.Exception@.  If you want the old versions you will need to+    import them qualified.++    This module only re-exports 'SomeException' from @Control.Exception@.+-}++-- | A proxy transformer that stores exceptions using 'EitherP'+type ExceptionP = EitherP SomeException++-- | Analogous to 'Ex.throwIO' from @Control.Exception@+throw :: (Monad m, P.Proxy p, Ex.Exception e) => e -> ExceptionP p a' a b' b m r+throw = E.throw . Ex.toException++-- | Analogous to 'Ex.catch' from @Control.Exception@+catch+ :: (Ex.Exception e, Monad m, P.Proxy p)+ => ExceptionP p a' a b' b m r         -- ^ Original computation+ -> (e -> ExceptionP p a' a b' b m r)  -- ^ Handler+ -> ExceptionP p a' a b' b m r+catch p f = p `E.catch` (\someExc ->+    case Ex.fromException someExc of+        Nothing -> E.throw someExc+        Just e  -> f e )++-- | Analogous to 'Ex.handle' from @Control.Exception@+handle+ :: (Ex.Exception e, Monad m, P.Proxy p)+ => (e -> ExceptionP p a' a b' b m r)  -- ^ Handler+ -> ExceptionP p a' a b' b m r         -- ^ Original computation+ -> ExceptionP p a' a b' b m r+handle = flip catch++data Status = Status {+    restore    :: forall a . IO a -> IO a,+    upstream   :: IORef (IO ())          ,+    downstream :: IORef (IO ())          }++{-| 'SafeIO' masks asynchronous exceptions by default, and only unmasks them+    during 'try' or 'tryIO' blocks in order to check all asynchronous+    exceptions.++    'SafeIO' also saves all finalizers dropped as a result of premature+    termination and runs them when the 'P.Session' completes.+-}+newtype SafeIO r = SafeIO { unSafeIO :: ReaderT Status IO r }++instance Functor SafeIO where+    fmap f m = SafeIO (fmap f (unSafeIO m))++instance Applicative SafeIO where+    pure r  = SafeIO (pure r)+    f <*> x = SafeIO (unSafeIO f <*> unSafeIO x)++instance Monad SafeIO where+    return r = SafeIO (return r)+    m >>= f  = SafeIO (unSafeIO m >>= \a -> unSafeIO (f a))++{-| Convert back to the 'IO' monad, running all dropped finalizers at the very+    end and rethrowing any checked exceptions++    This uses 'Ex.mask' to mask asynchronous exceptions and only unmasks them+    during 'try' or 'tryIO'.+-}+runSafeIO :: SafeIO (Either SomeException r) -> IO r+runSafeIO m =+    Ex.mask $ \restore -> do+        huRef <- newIORef (return ())+        hdRef <- newIORef (return ())+        e <- runReaderT (unSafeIO m) (Status restore huRef hdRef)+            `Ex.finally` (do+                hu <- readIORef huRef+                hu+                hd <- readIORef hdRef+                hd )+        case e of+            Left exc -> Ex.throwIO exc+            Right r  -> return r++{-| Convert back to the 'IO' monad, running all dropped finalizers at the very+    end and rethrowing any checked exceptions++    This uses 'Ex.uninterruptibleMask' to mask asynchronous exceptions and only+    unmasks them during 'try' or 'tryIO'.+-}+runSaferIO :: SafeIO (Either SomeException r) -> IO r+runSaferIO m =+    Ex.uninterruptibleMask $ \restore -> do+        huRef <- newIORef (return ())+        hdRef <- newIORef (return ())+        e <- runReaderT (unSafeIO m) (Status restore huRef hdRef)+            `Ex.finally` (do+                hu <- readIORef huRef+                hu+                hd <- readIORef hdRef+                hd )+        case e of+            Left exc -> Ex.throwIO exc+            Right r  -> return r++{-| Convert back to the 'IO' monad, running all dropped finalizers at the very+    end and preserving exceptions as 'Left's++    This uses 'Ex.mask' to mask asynchronous exceptions and only unmasks them+    during 'try' or 'tryIO'.+-}+trySafeIO :: SafeIO e -> IO e+trySafeIO m =+    Ex.mask $ \restore -> do+        huRef <- newIORef (return ())+        hdRef <- newIORef (return ())+        runReaderT (unSafeIO m) (Status restore huRef hdRef) `Ex.finally` (do+            hu <- readIORef huRef+            hu+            hd <- readIORef hdRef+            hd )++{-| Convert back to the 'IO' monad, running all dropped finalizers at the very+    end and preserving exceptions as 'Left's++    This uses 'Ex.uninterruptibleMask' to mask asynchronous exceptions and only+    unmasks them during 'try' or 'tryIO'.+-}+trySaferIO :: SafeIO e -> IO e+trySaferIO m =+    Ex.uninterruptibleMask $ \restore -> do+        huRef <- newIORef (return ())+        hdRef <- newIORef (return ())+        runReaderT (unSafeIO m) (Status restore huRef hdRef) `Ex.finally` (do+            hu <- readIORef huRef+            hu+            hd <- readIORef hdRef+            hd )+++{- I don't export registerK/register only because people rarely want to guard+   solely against premature termination.  Usually they also want to guard+   against exceptions, too.++    'registerK' should satisfy the following laws:++* 'registerK' defines a functor from finalizers to functions:++> registerK morph m1 . registerK morph m2 = registerK morph (m2 >> m1)+> +> registerK morph (return ()) = id++* 'registerK' is a functor between Kleisli categories:++> registerK morph m (p1 >=> p2) = registerK morph m p1 >=> registerK morph m p2+>+> registerK morph m return = return++    These laws are not provable using the current set of proxy laws, mainly+    because the proxy laws do not yet specify how proxies interact with the+    'Arrow' instance for the Kleisli category.  However, I'm reasonably sure+    that when I do specify this interaction that the above laws will hold.++    For now, just consider the above laws the contract for 'registerK' and+    consider any violations of the above laws as bugs.+-}+registerK+ :: (Monad m, P.Proxy p)+ => (forall x . SafeIO x -> m x)+ -> IO ()+ -> (b' -> p a' a b' b m r)+ -> (b' -> p a' a b' b m r)+registerK morph h k =+    P.runIdentityK (P.hoistK morph up) >-> k >-> P.runIdentityK (P.hoistK morph dn)+  where+    dn b'0 = do+        b0 <- P.request b'0+        huRef <- lift $ SafeIO $ asks downstream+        let dn' b = do+                hu <- lift $ SafeIO $ lift $ do+                    hu <- readIORef huRef+                    writeIORef huRef (hu >> h)+                    return hu+                b' <- P.respond b+                lift $ SafeIO $ lift $ writeIORef huRef hu+                b2 <- P.request b'+                dn' b2+        dn' b0+    up a'0 = do+        hdRef <- lift $ SafeIO $ asks upstream+        let up' a' = do+                hd  <- lift $ SafeIO $ lift $ do+                    hd <- readIORef hdRef+                    writeIORef hdRef (h >> hd)+                    return hd+                a   <- P.request a'+                lift $ SafeIO $ lift $ writeIORef hdRef hd+                a'2 <- P.respond a+                up' a'2+        up' a'0++register+ :: (Monad m, P.Proxy p)+ => (forall x . SafeIO x -> m x)+ -> IO ()+ -> p a' a b' b m r+ -> p a' a b' b m r+register morph h p = registerK morph h (\_ -> p) undefined+{- This is safe and there is a way that does not use 'undefined' if I slightly+   restructure the Proxy type class, but this will work for now. -}++{- $check+    The following @try@ functions are the only way to convert 'IO' actions to+    'SafeIO'.  These functions check all exceptions, including asynchronous+    exceptions, and store them in the 'ExceptionP' proxy transformer.+-}++{-| You can retroactively check all exceptions for proxies that implement+    'CheckP'.++    'try' is /almost/ a proxy morphism, which means that @tryK = (try .)@+    defines a functor that preserves five categories.  I say \"almost\" because+    it unmasks asynchronous exceptions for 'return'.  However, this does not+    affect the exception safety of this implementation.++    Functor between \'@K@\'leisli categories:++> tryK f >=> tryK g = tryK (f >=> g)+>+> tryK return = return  -- Not true for asynchronous exceptions++    Functor between 'P.Proxy' categories:++> tryK f >-> tryK g = tryK (f >-> g)+>+> tryK idT = idT++> tryK f >~> tryK g = tryK (f >~> g)+> +> tryK coidT = coidT++    Functor between \"request\" categories:++> tryK f \>\ tryK g = tryK (f \>\ g)+>+> tryK request = request++    Functor between \"respond\" categories:++> tryK f />/ tryK g = tryK (f />/ g)+>+> tryK respond = respond+-}+class (P.Proxy p) => CheckP p where+    try :: p a' a b' b IO r -> ExceptionP p a' a b' b SafeIO r++instance CheckP PF.ProxyFast where+    try p0 = EitherP (go p0) where+        go p = case p of+            PF.Request a' fa  -> PF.Request a' (\a  -> go (fa  a ))+            PF.Respond b  fb' -> PF.Respond b  (\b' -> go (fb' b'))+            PF.M m -> PF.M (SafeIO (ReaderT (\s -> do+                e <- Ex.try (restore s m)+                case e of+                    Left exc -> return (PF.Pure (Left exc))+                    Right p' -> return (go p') )))+            PF.Pure r -> PF.Pure (Right r)++instance CheckP PC.ProxyCorrect where+    try p0 = EitherP (go p0) where+        go p = PC.Proxy (SafeIO (ReaderT (\s -> do+            e <- Ex.try (restore s (PC.unProxy p))+            case e of+                Left exc -> return (PC.Pure (Left exc))+                Right fp -> case fp of+                    PC.Request a' fa  ->+                        return (PC.Request a' (\a  -> go (fa  a )))+                    PC.Respond b  fb' ->+                        return (PC.Respond b  (\b' -> go (fb' b')))+                    PC.Pure r -> return (PC.Pure (Right r)) )))++instance (CheckP p) => CheckP (IdentityP p) where+    try = EitherP . IdentityP . runEitherP . try . runIdentityP++instance (CheckP p) => CheckP (R.ReaderP i p) where+    try p = EitherP $ R.ReaderP $ \i -> runEitherP $ try (R.unReaderP p i)++-- | Check all exceptions for a 'P.Proxy' \'@K@\'leisli arrow+tryK+ :: (CheckP p)+ => (q -> p a' a b' b IO r) -> (q -> ExceptionP p a' a b' b SafeIO r)+tryK = (try .)++{-| Check all exceptions for an 'IO' action++> (tryIO .) f >=> (tryIO .) g = (tryIO .) (f >=> g)+>+> (tryIO .) return = return  -- Not true for asynchronous exceptions+-}+tryIO :: (P.Proxy p) => IO r -> ExceptionP p a' a b' b SafeIO r+tryIO io = EitherP $ P.runIdentityP $ lift $ SafeIO $ ReaderT $ \s ->+    Ex.try $ restore s io++{-| Similar to 'Ex.onException' from @Control.Exception@, except this also+    protects against:++    * premature termination, and++    * exceptions in other proxy stages.++    The first argument lifts 'onAbort' to work with other base monads.  Use+    'id' if your base monad is already 'SafeIO'.++> do x <- onAbort morph fin m+>    onAbort morph fin (f x)+> = onAbort morph fin $ do x <- m+>                          f x+>+> onAbort morph fin (return x) = return x++> onAbort morph fin1 . onAbort morph fin2 = onAbort morph (fin2 >> fin1)+>+> onAbort morph (return ()) = id+-}+onAbort+ :: (Monad m, P.Proxy p)+ => (forall x . SafeIO x -> m x)  -- ^ Monad morphism+ -> IO r'                         -- ^ Action to run on abort+ -> ExceptionP p a' a b' b m r    -- ^ Guarded computation+ -> ExceptionP p a' a b' b m r+onAbort morph after p =+    register morph (after >> return ()) p+        `E.catch` (\e -> do+            P.hoist morph $ tryIO after+            E.throw e )++{-| Analogous to 'Ex.finally' from @Control.Exception@++    The first argument lifts 'finally' to work with other base monads.  Use 'id'+    if your base monad is already 'SafeIO'.++> finally morph after p = do+>     r <- onAbort morph after p+>     hoist morph $ tryIO after+>     return r+-}+finally+ :: (Monad m, P.Proxy p)+ => (forall x . SafeIO x -> m x) -- ^ Monad morphism+ -> IO r'                        -- ^ Guaranteed final action+ -> ExceptionP p a' a b' b m r   -- ^ Guarded computation+ -> ExceptionP p a' a b' b m r+finally morph after p = do+    r <- onAbort morph after p+    P.hoist morph $ tryIO after+    return r++{-| Analogous to 'Ex.bracket' from @Control.Exception@++    The first argument lifts 'bracket' to work with other base monads.  Use 'id'+    if your base monad is already 'SafeIO'.++    'bracket' guarantees that if the resource acquisition completes, then the+    resource will be released.++> bracket morph before after p = do+>     h <- hoist morph $ tryIO before+>     finally morph (after h) (p h)+-}+bracket+ :: (Monad m, P.Proxy p)+ => (forall x . SafeIO x -> m x)       -- ^ Monad morphism+ -> IO h                               -- ^ Acquire resource+ -> (h -> IO r')                       -- ^ Release resource+ -> (h -> ExceptionP p a' a b' b m r)  -- ^ Use resource+ -> ExceptionP p a' a b' b m r+bracket morph before after p = do+    h <- P.hoist morph $ tryIO before+    finally morph (after h) (p h)++{-| Analogous to 'Ex.bracket_' from @Control.Exception@++    The first argument lifts 'bracket_' to work with any base monad.  Use 'id'+    if your base monad is already 'SafeIO'.++> bracket_ morph before after p = do+>     hoist morph $ tryIO before+>     finally morph after p+-}+bracket_+ :: (Monad m, P.Proxy p)+ => (forall x . SafeIO x -> m x)  -- ^ Monad morphism+ -> IO r1                         -- ^ Acquire resource+ -> IO r2                         -- ^ Release resource+ -> ExceptionP p a' a b' b m r    -- ^ Use resource+ -> ExceptionP p a' a b' b m r+bracket_ morph before after p = do+    P.hoist morph $ tryIO before+    finally morph after p++{-| Analogous to 'Ex.bracketOnAbort' from @Control.Exception@++    The first argument lifts 'bracketOnAbort' to work with any base monad.  Use+    'id' if your base monad is already 'SafeIO'.++> bracketOnAbort morph before after p = do+>     h <- hoist morph $ tryIO before+>     onAbort morph (after h) (p h)+-}+bracketOnAbort+ :: (Monad m, P.Proxy p)+ => (forall x . SafeIO x -> m x)       -- ^ Monad morphism+ -> IO h                               -- ^ Acquire resource+ -> (h -> IO r')                       -- ^ Release resource+ -> (h -> ExceptionP p a' a b' b m r)  -- ^ Use resource+ -> ExceptionP p a' a b' b m r+bracketOnAbort morph before after p = do+    h <- P.hoist morph $ tryIO before+    onAbort morph (after h) (p h)++{- $prompt+    This implementation will not /promptly/ finalize a 'P.Proxy' if another+    composed 'P.Proxy' prematurely terminates.  However, the implementation will+    still save the dropped finalizer and run it when the 'P.Session' completes+    in order to guarantee deterministic finalization.++    To see why, consider the following 'P.Proxy' assembly:++> p1 >-> ((p2 >-> p3) >=> p4)++    Now ask yourself the question, \"If @p3@ prematurely terminates, should it+    promptly finalize @p1@?\"++    If you answered \"yes\", then you would have a bug if @p4@ were to+    'request', which would restore control to @p1@ after we already finalized+    it.++    If you answered \"no\", then consider the case where @p2 = idT@ and+    @p4 = return@:++> p1 >-> ((idT >-> p3) >=> return)+> p1 >-> (idT >-> p3)               -- f   >=> return = f+> p1 >-> p3                         -- idT >-> p      = p++    Answering \"no\" means that @p3@ would be unable to promptly finalize a+    'P.Proxy' immediately upstream of it.++    There is a solution that permits perfectly prompt finalization, but it+    requires indexed monads to guarantee the necessary safety through the type+    system.  In the absence of indexed monads, the next best solution is to let+    you promptly finalize things yourself, but then /you/ must prove that this+    finalization is safe and that all upstream pipes are unreachable.++    The following two unsafe operations allow you to trade safety for prompt+    finalization.  Use them if you desire prompter finalization guarantees and+    if you can prove their usage is safe.  However, this proof is not trivial.++    For example, you might suppose that the following usage of 'unsafeCloseU' is+    safe because it never 'request's after closing upstream, nor does it+    terminate:++> falseSenseOfSecurity () = do+>     x <- request ()+>     unsafeCloseU+>     forever $ respond x++    However, this is not safe, as the following counter-example demonstrates:++> p1 >-> ((falseSenseOfSecurity >-> request) >=> request)++    @falseSenseOfSecurity@ will finalize the upstream @p1@, but then will+    abort when the downstream 'request' terminates, and then the second+    'request' will illegally access @p1@ after we already finalized it.++    In other words, you cannot prove any prompt finalization is safe unless you+    control the entire 'P.Session'.  Therefore, do not use the following unsafe+    operations in 'P.Proxy' libraries.  Only the end user assembling the+    final 'P.Session' may safely insert these calls.+-}++{-| 'unsafeCloseU' calls all finalizers registered upstream of the current+    'P.Proxy'. -}+unsafeCloseU :: (P.Proxy p) => r -> ExceptionP p a' a b' b SafeIO r+unsafeCloseU r = do+    (huRef, hu) <- lift $ SafeIO $ do+        huRef <- asks upstream+        hu    <- lift $ readIORef huRef+        return (huRef, hu)+    tryIO hu+    lift $ SafeIO $ lift $ writeIORef huRef (return ())+    return r++{-| 'unsafeCloseD' calls all finalizers registered downstream of the current+    'P.Proxy'. -}+unsafeCloseD :: (P.Proxy p) => r -> ExceptionP p a' a b' b SafeIO r+unsafeCloseD r = do+    (hdRef, hd) <- lift $ SafeIO $ do+        hdRef <- asks downstream+        hd    <- lift $ readIORef hdRef+        return (hdRef, hd)+    tryIO hd+    lift $ SafeIO $ lift $ writeIORef hdRef (return ())+    return r++{-| 'unsafeClose' calls all registered finalizers++    'unsafeClose' is a Kleisli arrow so that you can easily seal terminating+    proxies if there is a risk of delayed finalization:++> (producer >-> (takeB_ 10 >=> unsafeClose) >-> consumer) >=> later+-}+unsafeClose :: (P.Proxy p) => r -> ExceptionP p a' a b' b SafeIO r+unsafeClose = unsafeCloseU P.>=> unsafeCloseD
+ Control/Proxy/Safe/Tutorial.hs view
@@ -0,0 +1,484 @@+{-| This module provides the tutorial for the @pipes-safe@ library++    This tutorial assumes that you have already read the main @pipes@ tutorial+    in @Control.Proxy.Tutorial@.+-}++module Control.Proxy.Safe.Tutorial (+    -- * Introduction+    -- $intro++    -- * Resource Safety+    -- $safety++    -- * Native Exception Handling+    -- $native++    -- * Checked Exceptions+    -- $checked++    -- * Prompt Finalization+    -- $prompt++    -- * Upgrade Proxy Transformers+    -- $trytransformer++    -- * Backwards Compatibility+    -- $backwards++    -- * Laziness+    -- $laziness++    -- * Conclusion+    -- $conclusion+    ) where++import Control.Proxy+import Control.Proxy.Safe+import System.IO (withFile)++{- $intro+    @pipes-safe@ adds exception-safe resource management to the @pipes@+    ecosystem.  Use this library if you want to:++    * Safely acquire and release resources within proxies++    * Natively catch and handle exceptions, including asynchronous exceptions++    The following example shows how to use 'P.Proxy' resource management to+    safely open and close a file:++> import Control.Monad (unless)+> import Control.Proxy+> import Control.Proxy.Safe+> import System.IO+> +> readFileS+>  :: (Proxy p) => FilePath -> () -> Producer (ExceptionP p) String SafeIO ()+> readFileS file () = bracket id+>     (do h <- openFile file ReadMode+>         putStrLn $ "{File Open}"+>         return h )+>     (\h -> do putStrLn "{Closing File}"+>               hClose h )+>     (\h -> let loop = do+>                    eof <- tryIO $ hIsEOF h+>                    unless eof $ do+>                        str <- tryIO $ hGetLine h+>                        respond str+>                        loop+>             in loop )++    @readFileS@ uses 'bracket' from "Control.Proxy.Safe" to guard the file+    handle, which imposes two constraints on the type:++    * 'bracket' requires the 'ExceptionP' proxy transformer in order to handle+      exceptions++    * 'bracket' requires 'SafeIO' as the base monad, which checks all+      asynchronous exceptions and stores registered finalizers++    But what if we already wrote a 'Consumer' that doesn't use 'ExceptionP' or+    'SafeIO'?++> printer :: (Proxy p, Show a) => () -> Consumer p a IO r+> printer () = runIdentityP $ forever $ do+>     a <- request ()+>     lift $ print a++    Do we need to rewrite it to use resource management abstractions?  Not at+    all!  We can use 'try' / 'tryK' to automatically promote any \"unmanaged\"+    proxy to a \"managed\" proxy:++> tryK+>  :: CheckP p+>  => (q -> p a' a b' b IO r) -> q -> ExceptionP p a' a b' b SafeIO r +>+> tryK printer :: (CheckP p, Show a) => () -> Consumer (Exception p) a SafeIO r+>+> session :: (CheckP p) => () -> Session (Exception p) SafeIO ()+> session = readFileS "test.txt" >-> tryK printer++    The 'CheckP' constraint indicates that the base 'Proxy' type must be+    promotable using 'try'.++    To run this 'Session', we unwrap each layer:++>>> runSafeIO $ runProxy $ runEitherK session :: IO ()+{File Open}+"Line 1"+"Line 2"+"Line 3"+"Line 4"+{Closing File}++-}++{- $safety+    'bracket' guarantees that every successful resource acquisition is paired+    with finalization, even in the face of exceptions or premature 'Session'+    termination.++    For example, if we only draw two lines of input, 'bracket' will still safely+    finalize the handle:++> main = runSafeIO $ runProxy $ runEitherK $+>     readFileS "test.txt" >-> takeB_ 2 >-> tryK printD++>>> main+{File Open}+"Line 1"+"Line 2"+{Closing File}++    We can even sabotage ourselves by killing our own thread after a delay:++> import Control.Concurrent+>+> main = do+>     tID <- myThreadId+>     forkIO $ do+>         threadDelay 1000+>         killThread tID+>     runSafeIO $ runProxy $ runEitherK $+>         foreverK (readFileS "test.txt") >-> tryK printD++>>> main+...+"Line 2"+"Line 3"+"Line 4"+{Closing File}+{File Open}+"Line 1"+"Line 2"+{Closing File}+*** Exception: thread killed++    ... yet 'bracket' still ensures deterministic resource finalization in the+    face of asynchronous exceptions.+-}++{- $native+    Let's study the types a bit to understand what is going on:++> type ExceptionP = EitherP SomeException++    'ExceptionP' is just a type synonym around 'EitherP'.  @pipes-safe@ uses+    'EitherP' to check all exceptions in order to take advantage of the ability+    to 'catch' and 'throw' exceptions locally.  In fact, "Control.Proxy.Safe"+    defines specialized versions of 'throw' and 'catch' that mirror their+    equivalents in @Control.Exception@:++> throw :: (Monad m, Proxy p, Exception e) => e -> ExceptionP p a' a b' b m r+>+> catch+>  :: (Monad m, Proxy p, Exception e)+>  => ExceptionP p a' a b' b m r+>  -> (e -> ExceptionP p a' a b' b m r)+>  -> ExceptionP p a' a b' b m r++    These let you embed native exception handling into proxies.  For example,+    we could exception handling to recover from a file opening error:++> import Prelude hiding (catch) -- if using base <= 4.5+>+> openFileS :: (CheckP p) => () -> Producer (ExceptionP p) String SafeIO ()+> openFileS () = (do+>     tryIO $ putStrLn "Select a file:"+>     file <- tryIO getLine+>     readFileS file ())+>   `catch` (\e -> do+>       tryIO $ print (e :: IOException)+>       openFileS () )++>>> runSafeIO $ runProxy $ runEitherK $ openFileS >-> tryK printD+Select a file:+oops+oops: openFile: does not exist (No such file or directory)+Select a file:+test.txt+{File Open}+"Line 1"+"Line 2"+"Line 3"+"Line 4"+{Closing File}++    You can even catch and resume from asynchronous exceptions:++> heartbeat+>   :: Proxy p+>   => ExceptionP p a' a b' b SafeIO r -> ExceptionP p a' a b' b SafeIO r+> heartbeat p = p `catch` (\e -> do+>            let _ = e :: SomeException+>            tryIO $ putStrLn "<Nice try!>"+>            heartbeat p )+>+> main = do+>     tid <- myThreadId+>     forkIO $ forever $ do+>         threadDelay 5000000  -- Every 5 seconds+>         killThread tid+>     trySafeIO $ runProxy $ runEitherK $+>         heartbeat . (openFileS >-> tryK printD)++>>> main+Select a file:+te<Nice Try!>+Select a file:+st.txt+{File Open}+"Line 1"+"Line 2"+"Line 3"+"Line 4"+{Closing File}++-}++{- $checked+    Exception handling works because 'SafeIO' checks all exceptions and stores+    them using the 'ExceptionP' proxy transformer.  'SafeIO' masks all+    asynchronous exceptions by default and only unmasks them in the middle of a+    'try' or 'tryIO' block.  This prevents asynchronous exceptions from leaking+    between the cracks.++    'runSafeIO' reraises the stored exception when the 'Session' completes, but+    you can also choose to preserve the exception as a 'Left' by using+    'trySafeIO' instead:++> main = do+>     tID <- myThreadId+>     forkIO $ do+>         threadDelay 1000+>         killThread tID+>     runSafeIO $ runProxy $ runEitherK $+>         foreverK (readFileS "test.txt") >-> tryK printD++>>> main+...+"Line 2"+"Line 3"+"Line 4"+{Closing File}+{File Open}+"Line 1"+"Line 2"+{Closing File}+Left thread killed++    You can even choose whether to use 'mask' or 'uninterruptibleMask':++    * 'runSafeIO' and 'trySafeIO' both use 'mask'++    * 'runSaferIO' and 'trySaferIO' both use 'uninterruptibleMask'.+-}++{- $prompt+    Resource management primitives like 'bracket' only guarantee prompt+    finalization in the face of exceptions.  Premature termination of+    composition will delay the finalizer until the end of the 'Session'.++    For example, consider the following 'Session':++> session () = do+>    (readFileS "test.hs" >-> takeB_ 2 >-> tryK printD) ()+>    tryIO $ putStrLn "Look busy"++>>> runSafeIO $ runProxy $ runEitherK session+{File Open}+"Line 1"+"Line 2"+Look busy+{Closing File}++    @readFileS@ is interrupted when @takeB_@ terminates, so it does not get+    finalized until the very end of the 'Session'.++    The \"Prompt Finalization\" section of "Control.Proxy.Safe" documents why+    this behavior is the only safe default.  However, often we can prove that+    prompter finalization is safe, in which case we can take matters into our+    own hands and manually finalize things even more promptly:++> session () = do+>     (readFileS "test.hs" >-> (takeB_ 2 >=> unsafeClose) >-> tryK printD) ()+>     tryIO $ putStrLn "Look busy"++>>> runSafeIO $ runProxy $ runEitherK session+{File Open}+"import Control.Concurrent"+"import Control.Monad (unless)"+{Closing File}+Look busy++    Fortunately, most of the time you will just assemble linear composition+    chains that look like this:++> runSafeIO $ runProxy $ runEitherK $ p1 >-> p2 >-> p3 >-> p4++    ... in which case the end of composition coincides with the end of the+    'Session' and there is no delay in finalization.  You only need to manually+    manage prompt finalization if you sequence anything after composition.+-}++{- $trytransformer+    Not all proxy transformers implement 'try'.  You can look at the instance+    list for 'CheckP' and you will see that it mainly covers the base+    proxy implementations:++> instance CheckP ProxyFast+> instance CheckP ProxyCorrect+> instance (CheckP p) => CheckP (IdentityP p)+> instance (CheckP p) => CheckP (ReaderP   p)++    However, you actually can upgrade more sophisticated proxy transformer+    stacks.  I've relaxed the type signature of the 'PFunctor' type class to+    accept proxy morphisms that change the base monad, so now it accepts 'try'+    as a valid argument.  This means that you can use 'hoistP' as many times as+    necessary to target 'try' to the base proxy:++> p :: (CheckP p) => Producer (StateP s (MaybeP p)) IO r+>+> hoistP (hoistP try) p+>   :: (CheckP p) => Producer (StateP s (MaybeP (ExceptionP p))) SafeIO r++    'hoistP' expects a proxy morphism for its argument, but is 'try' a proxy+    morphism?  Yes!  'try' satisfies the proxy morphism laws, which are the same+    as the proxy transformer laws.  The documentation for 'try' lists the full+    set of equations, but the ones you should remember are:++> tryK (f >-> g) = tryK f >-> tryK g++> try (request a') = request a'++> try (respond b ) = respond b++>   do x <- try m+>      try (f x)+> = try $ do x <- m+>            f x+>+> try (return x) = return x -- Almost true!++    The last equation is slightly incorrect.  The left hand-side may throw an+    asynchronous exception, but the right-hand side will not.  This does not+    compromise the safety of this library.  At worst, it will just overzealously+    mask pure segments of code if you don't wrap them in 'try', which just+    delays the asynchronous exception until the next 'IO' action.++    There is one upgrade scenario that this library does not yet cover, which is+    upgrading proxies that have base monads other than 'IO'.  For now, you will+    have to rewrite the proxy if that happens.+-}++{- $backwards+    The biggest strength of @pipes-safe@ is that it requires no buy-in from the+    rest of the @pipes@ ecosystem.  Many proxies require no resource-management+    at all, so why should they clutter their implementation with such concerns?++    @pipes-safe@ lets you write these proxies using the simpler \"unmanaged\"+    types, and then transparently promote them with 'try' later on if you need+    to use them within a resource-managed 'Session'.++    For example, the main body of @readFileS@ is identical to the implementation+    of 'hGetLineS' from the proxy prelude:++> hGetLineS :: (Proxy p) => Handle -> () -> Producer p String IO ()+> hGetLineS h () = runIdentityP loop where+>     loop = do+>         eof <- lift $ hIsEOF h+>         if eof+>             then return ()+>             else do+>                 str <- lift $ hGetLine h+>                 respond str+>                 loop++    We can reuse 'hGetLineS' by define a 'withFileS' that abstracts away the+    handle management:++> withFileS+>  :: (Proxy p)+>  => FilePath+>  -> (Handle -> b' -> ExceptionP p a' a b' b SafeIO r)+>  -> b' -> ExceptionP p a' a b' b SafeIO r+> withFileS file p b' = bracket id+>     (do h <- openFile file ReadMode+>         putStrLn "{File Open}"+>         return h )+>     (\h -> do putStrLn "{Closing File}"+>               hClose h )+>     (\h -> p h b')++    ... and now we can 'readFileS' in terms of 'withFileS' and 'hGetLineS':++> readFileS file = withFileS file (\h -> tryK (hGetLineS h))++    If 'hGetLineS' throws an error within its own code, 'withFileS' will still+    properly finalize the handle.  This works in spite of 'hGetLineS' never+    having been written to be resource safe.+-}++{- $laziness+    Now you no longer need to open all resources before a 'Session' and close+    them afterwards.  Instead, you can lazily open resources in response to+    demand and trust that they finalize safely upon termination:++> files () = do+>     readFileS "file1.txt" () -- 3 lines long+>     readFileS "file2.txt" () -- 4 lines long+> -- or: files = readFileS "file1.txt" >=> readFileS "file2.txt"++>>> runSafeIO $ runProxy $ runEitherK $ files >-> takeB_ 2 >-> printD+{File Open}+"Line 1 of file1.txt"+"Line 2 of file1.txt"+{Closing File}++    @\"file2.txt\"@ never opens because we only demand two lines.++    Even if we use both files, we never keep more than one handle open at a+    time:+ +>>> runSafeIO $ runProxy $ runEitherK $ files >-> takeB_ 5 >-> printD+{File Open}+"Line 1 of file1.txt"+"Line 2 of file1.txt"+"Line 3 of file1.txt"+{Closing File}+{File Open}+"Line 1 of file2.txt"+"Line 2 of file2.txt"+{Closing File}++-}++{- $conclusion+    @pipes-safe@ lets you package streaming resources into self-contained units+    that include:++    * their allocation/deallocation code, and++    * their exception-handling strategies.++    @pipes-safe@ reuses 'EitherP' to let you easily reason about how local+    exception handling behaves.  More importantly, multiple resources can+    concurrently coexist with each other and not interfere with each other's+    exception-handling logic.  @pipes-safe@ isolates each streaming component's+    behavior so that you reason about it how it deals with failure independently+    of other components.++    I hope this inspires people to package up more powerful streaming+    abstractions into indivisible units.  Also, don't limit yourself to simple+    file or network resources.  You will find that @pipes-safe@ can also+    simplify and package up:++    * progress meters,++    * input devices (i.e. mice and keyboards), and++    * user interfaces.++    I encourage you to be creative!+-}
+ LICENSE view
@@ -0,0 +1,24 @@+Copyright (c) 2013, Gabriel Gonzalez+All rights reserved.++Redistribution and use in source and binary forms, with or without modification,+are permitted provided that the following conditions are met:+    * Redistributions of source code must retain the above copyright notice,+      this list of conditions and the following disclaimer.+    * Redistributions in binary form must reproduce the above copyright notice,+      this list of conditions and the following disclaimer in the documentation+      and/or other materials provided with the distribution.+    * Neither the name of Gabriel Gonzalez nor the names of other contributors+      may be used to endorse or promote products derived from this software+      without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ pipes-safe.cabal view
@@ -0,0 +1,45 @@+Name: pipes-safe+Version: 1.0.0+Cabal-Version: >=1.14.0+Build-Type: Simple+License: BSD3+License-File: LICENSE+Copyright: 2013 Gabriel Gonzalez+Author: Gabriel Gonzalez+Maintainer: Gabriel439@gmail.com+Bug-Reports: https://github.com/Gabriel439/Haskell-Pipes-Safe-Library/issues+Synopsis: Safety for the pipes ecosystem+Description:+    This package adds resource management and exception handling to the @pipes@+    ecosystem.  Notable features include:+    .+    * /Resource Safety/: Guarantee finalization using @finally@, @bracket@ and+      more+    .+    * /Laziness/: Only acquire resources when you need them+    .+    * /Exception safe/: Even against asynchronous exceptions!+    .+    * /Native Exception Handling/: Catch and resume from exceptions within any+      @Session@+    .+    * /No Buy-in/: Managed pipes play nice with unmanaged pipes+    .+    Import @Control.Proxy.Safe@ to use this library+    .+    Read @Control.Proxy.Safe.Tutorial@ for an introductory tutorial.+Category: Control, Pipes, Proxies+Tested-With: GHC ==7.4.1+Source-Repository head+    Type: git+    Location: https://github.com/Gabriel439/Haskell-Pipes-Safe-Library++Library+    Build-Depends:+        base >= 4 && < 5,+        transformers >= 0.2.0.0,+        pipes >= 3.1.0+    Exposed-Modules:+        Control.Proxy.Safe,+        Control.Proxy.Safe.Tutorial+    Default-Language: Haskell98