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 +577/−0
- Control/Proxy/Safe/Tutorial.hs +484/−0
- LICENSE +24/−0
- Setup.hs +2/−0
- pipes-safe.cabal +45/−0
+ 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