{-| 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 use 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 "" > 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 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 > trySafeIO $ 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 and trivial proxy transformers: > instance CheckP ProxyFast > instance CheckP ProxyCorrect > instance (CheckP p) => CheckP (IdentityP p) > instance (CheckP p) => CheckP (ReaderP p) This means that we can usually only 'try' the base proxy. However, this is not a problem because we can just 'hoistP' 'try' over the outer proxy transformers to target it to the base proxy. For example, if we have a 'Proxy' with two proxy transformer layers: > p :: (CheckP p) => Producer (StateP s (MaybeP p)) IO r ... we just 'hoistP' the 'try' over the two outer layers to target it to the base 'Proxy': > 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 and the documentation in the @Control.Proxy.Morph@ module (from the @pipes@ package) lists the full set of laws. The important laws 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 'try' or 'tryIO' block. There is one upgrade scenario that this library does not yet cover, which is 'try'ing 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! -}