John Wiegley and I are currently working- together with some members of the community- on various exception handling related code. As part of this work, we're also going to be publishing a number of tutorials on this topic. Eventually, we'll collect all of this information together into a more cohesive whole, but for now we wanted to get the content out there to the community as we produce it.
Love them or hate them, runtime exceptions are a reality of writing Haskell code. Consider the following fake code:
myFunc = do
resource <- acquireScarceResource
useResource resource
releaseResource resource
In a world without exceptions, this seems completely reasonable. However, what happens if the call to useResource
throws a runtime exception? In such a case, releaseResource
would never be called, and our scarce resource would never be released. In concurrent code dealing with mutexes, such a bug could lead to a deadlock.
The solution to this problem is well known: use the bracket
function. Then our above example reduces to myFunc = bracket acquireScarceResource releaseResource useResource
. We're now safe from any exception thrown by useResource
, and even from asynchronous exceptions (a topic I'll try to avoid in this post).
But let's analyze the type signature of bracket
:
bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c
How do we use such a function in monad transformer stack, where the functions we pass in won't live in the IO
monad itself?
Note that for the rest of this tutorial, I'm going to talk about the finally
function instead of bracket
, which is really just a simplified version of the latter. The reasoning used will apply to not only bracket
, but also to many other functions in Control.Exception
. Its signature is IO a -> IO b -> IO a
, and its purpose is to ensure that the second function is called regardless of what exceptions are thrown by the first (or any asynchronous exceptions... sorry for bringing those up again).
The simple case: the ReaderT transformer
Let's forget about any kind of grand solution. Instead, let's take one of the simplest monad transformers that exists, ReaderT
, and see how we can write a finally
that works with it. Since a ReaderT
simply passes in some environment to all actions, this turns out to be really easy:
import Control.Monad.Reader
import Control.Exception (finally, try, ErrorCall)
finallyReaderT :: ReaderT r IO a -> ReaderT r IO b -> ReaderT r IO a
finallyReaderT a sequel = do
r <- ask
liftIO $ runReaderT a r `finally` runReaderT sequel r
main :: IO ()
main = do
res <- try $ runReaderT
(step1 `finallyReaderT` step2)
"This is the environment"
print (res :: Either ErrorCall ())
where
step1 = do
r <- ask
liftIO $ putStrLn $ "In step1, r == " ++ show r
error "Erroring out, bye!"
step2 = do
r <- ask
liftIO $ putStrLn $ "In step2, r == " ++ show r
That turns out to be pretty simple. And by reusing the existing finally
function, we can feel comfortable knowing that our implementation is correct (or at least as correct as finally
itself).
First complication: mutable state
But ReaderT
is a really simple case to support, since it's just a read-only environment. Let's up the ante a bit, and try using StateT
instead. We'll now need to deal with threading the mutable state variable through. Before jumping into the code, let's consider abstractly how would could achieve this threading. We have two possible cases: an exception is thrown before calling the cleanup function, or an exception is not thrown. If no exception is thrown, then we can extract the state value from the result of the first function call, pass that to the cleanup function, and then return the final state value as the new mutable state.
But if an exception is thrown, there won't be a chance to extract the updated state value from the first function. In that case, we'll only have the initial state value available to pass through to the cleanup function. (The new result state is irrelevant, since the exception is going to be rethrown anyway.) As described until now, let's see what this kind of finally
implementation may look like.
import Prelude hiding (catch)
import Control.Monad.State
import Control.Exception
finallyStateT :: StateT s IO a -> StateT s IO b -> StateT s IO a
finallyStateT a sequel = do
s1 <- get
(result, s2) <- liftIO $ runStateT a s1 `catch` \e -> do
_ignored <- runStateT sequel s1
throwIO (e :: SomeException)
(_ignored, s3) <- liftIO $ runStateT sequel s2
put s3
return result
main :: IO ()
main = do
res <- try $ runStateT
(step1 `finallyStateT` step2)
1
print (res :: Either ErrorCall ((), Int))
where
step1 = do
s <- get
liftIO $ putStrLn $ "In step1, s == " ++ show s
put $ s + 1
error "Erroring out, bye!" -- Try commenting me out
step2 = do
s <- get
liftIO $ putStrLn $ "In step2, s == " ++ show s
put $ s + 1
This seems to work, but there are a two problems:
- We were not able to reuse the standard
finally
function to implement this, since it wouldn't allow us to manually thread state. Practically, that means our implementation is susceptible to asynchronous exceptions (argh... those came up again). - The behavior is pretty inconsistent. In one case, we use the original mutable state value and ignore the updated state from the cleanup function. In the other, we use the updated state value from the first function and keep the updated mutable state.
So let's instead try out a different approach. Regardless of whether an exception is thrown or not, we'll call the cleanup function using the original mutable state, and always ignore the state produced by the cleanup function.
import Control.Monad.State
import Control.Exception
finallyStateT :: StateT s IO a -> StateT s IO b -> StateT s IO a
finallyStateT a sequel = do
s1 <- get
(result, s2) <- liftIO $ runStateT a s1 `finally`
runStateT sequel s1
put s2
return result
main :: IO ()
main = do
res <- try $ runStateT
(step1 `finallyStateT` step2)
1
print (res :: Either ErrorCall ((), Int))
where
step1 = do
s <- get
liftIO $ putStrLn $ "In step1, s == " ++ show s
put $ s + 1
error "Erroring out, bye!" -- Try commenting me out
step2 = do
s <- get
liftIO $ putStrLn $ "In step2, s == " ++ show s
put $ s + 1
This solves my two complaints from before, but unfortunately introduces a new one: the behavior is not really intuitive in the non-exception case. Nonetheless, in my opinion, this is the right way to implement things due to the consistency. We'll cover a method to get back the more intuitive semantics towards the end of this post.
Second complication: alternative exit paths
The previous section essentially provided two approaches to writing a transformer-based finally
: base it off of primitives like catch
, or reuse finally
. It turns out that both of these approaches have quite a bit of prior art. In the first case, there are the MonadCatchIO-mtl
and MonadCatchIO-transformers
packages, as well as the more recent exceptions
package. These all define a typeclass for the primitive operations of catch
ing and masking asynchronous exceptions (we just can't escape those, can we?).
Let's extract the catch
logic from our first StateT
example above to demonstrate the technique:
import Prelude hiding (catch)
import Control.Monad.State
import Control.Exception
-- show
catchStateT :: Exception e
=> StateT s IO a
-> (e -> StateT s IO a)
-> StateT s IO a
catchStateT a onE = do
s1 <- get
(result, s2) <- liftIO $ runStateT a s1 `catch` \e ->
runStateT (onE e) s1
put s2
return result
finallyStateT :: StateT s IO a -> StateT s IO b -> StateT s IO a
finallyStateT a sequel = do
result <- a `catchStateT` \e -> do
_ignored <- sequel
liftIO $ throwIO (e :: SomeException)
_ignored <- sequel
return result
-- /show
main :: IO ()
main = do
res <- try $ runStateT
(step1 `finallyStateT` step2)
1
print (res :: Either ErrorCall ((), Int))
where
step1 = do
s <- get
liftIO $ putStrLn $ "In step1, s == " ++ show s
put $ s + 1
error "Erroring out, bye!" -- Try commenting me out
step2 = do
s <- get
liftIO $ putStrLn $ "In step2, s == " ++ show s
put $ s + 1
That's certainly a bit prettier than what we had before (though it's still not async-exception safe). And what's really nice is that this definition of finallyStateT
doesn't actually have anything StateT
-specific about it. So presuming we had a catch
function for some other transformer, we could reuse the same definition. Let's try this out for ErrorT
.
import Prelude hiding (catch)
import Control.Monad.Error
import Control.Exception
catchErrorT :: (Exception e, Error s)
=> ErrorT s IO a
-> (e -> ErrorT s IO a)
-> ErrorT s IO a
catchErrorT a onE = do
eresult <- liftIO $ runErrorT a `catch` \e ->
runErrorT (onE e)
either throwError return eresult
finallyErrorT :: Error s => ErrorT s IO a -> ErrorT s IO b -> ErrorT s IO a
finallyErrorT a sequel = do
result <- a `catchErrorT` \e -> do
_ignored <- sequel
liftIO $ throwIO (e :: SomeException)
_ignored <- sequel
return result
main :: IO ()
main = do
res <- try $ runErrorT
(step1 `finallyErrorT` step2)
print (res :: Either ErrorCall (Either String ()))
where
step1 = do
liftIO $ putStrLn $ "In step1"
throwError "This should be interesting"
step2 = liftIO $ putStrLn $ "In step2"
Go ahead and run that code snippet. Do you notice something not there to be noticed? That's right, the cleanup function is never called! Here we have a function called finally
which, despite its name, doesn't actually call the cleanup function when it finally exits. What gives?
The issue is that, with an ErrorT s IO
stack, there are three ways to exit the function: normally, with a runtime exception, or with a throwError
result. In finallyErrorT
, the call to catchErrorT
accounts for the runtime exception case, and the following call to sequel
accounts for the normal exit case. But there's no way to account for the throwError
case without adding ErrorT
-specific logic to finally
(we'll do that in a moment).
You can try this example out with MonadCatchIO-mtl
or MonadCatchIO-transformers
and reproduce this buggy behavior. This affects both the ErrorT
and ContT
transformers. In the exceptions
package, this also applies to the CatchT
type. (John Wiegley discussed this issue with Edward, who described this as a documentation bug, since CatchT
should not be used on top of IO
.)
Doing this correctly is certainly possible, as you can see with the following example:
import Control.Monad.Error
import Control.Exception
-- show
finallyErrorT :: Error s => ErrorT s IO a -> ErrorT s IO b -> ErrorT s IO a
finallyErrorT a sequel = do
eresult <- liftIO $ runErrorT a `finally` runErrorT sequel
either throwError return eresult
-- /show
main :: IO ()
main = do
res <- try $ runErrorT
(step1 `finallyErrorT` step2)
print (res :: Either ErrorCall (Either String ()))
where
step1 = do
liftIO $ putStrLn $ "In step1"
throwError "This should be interesting"
step2 = liftIO $ putStrLn $ "In step2"
But this seems to imply that we would need to write a separate implementation of finally
for each and every transformer, which would be tedious and error-prone. There must be a better way.
monad-control
If you look at our various implementations so far, they all follow a similar pattern: embedding the state of the monad inside the value, running the underlying monadic action, and then rebuilding the monadic state from the result. This procedure can be done with many different transformers (basically, all common transformers except ContT
). To capture this concept, we have the MonadTransControl
and MonadBaseControl
typeclasses in the monad-control
package.
monad-control
is frankly pretty complicated, and I'm not going to delve into all of its details here. Instead, I'll point you to the lifted-base
package, which uses monad-control
to create transformer-friendly versions of many common functions in base
. As you'd expect based on this tutorial, exception handling functions are present, but also concurrency, timeouts, and mutable variables (which will become important in just a moment).
monad-control
is- as far as I know- the third attempt at a library to cover these concepts. Previous solutions were monad-peel
and my own MonadInvertIO, part of earlier versions of neither
. While in my opinion these other two approaches are easier to understand, monad-control
is highly optimized, and therefore gets my recommendation as the go-to library for handling monad transformer stacks.
More intuitive mutable state
We're left with one annoyance with the monad-control
-based solution: we lose any mutations performed to our mutable state either before an exception is thrown, or in our cleanup functions. Is there some way to keep the elegance of monad-control
but get back the more intuitive behavior? The answer is yes, but you may not like it.
When dealing with either StateT
or WriterT
transformers, the behavior which I would consider most intuitive would be that any mutations are immediately captured. Unfortunately, this can't be achieved at all using the standard StateT
and WriterT
implementations, since the monadic state is thrown away as soon as an exception is thrown.
In this case, my solution is to use a mutable variable- such as an IORef
- and keep a reference to it in a ReaderT
. As an example, this technique is used by the HandlerT
transformer used in Yesod.
To demonstrate the concept, here's an implementation of the RWST monad transformer which uses IORef
s for holding mutable state. For completeness, I'll include appropriate instances for monad-control
and demonstrate usage of lifted-base
.
{-# OPTIONS_GHC -Wall -Werror #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
import Control.Monad.RWS.Class
import Control.Monad.Reader
import Control.Monad.Base
import Control.Applicative (Applicative)
import Data.IORef.Lifted
import Data.Monoid (Monoid, (<>), mempty)
import Control.Exception.Lifted
import Control.Monad.Trans.Control
data Env r w s = Env
{ envReader :: !r
, envWriter :: !(IORef w)
, envState :: !(IORef s)
}
newtype RWST r w s m a = RWST (ReaderT (Env r w s) m a)
deriving (Functor, Applicative, Monad, MonadIO, MonadTrans)
instance Monad m => MonadReader r (RWST r w s m) where
ask = RWST $ liftM envReader ask
local f (RWST g) =
RWST $ local f' g
where
f' env = env { envReader = f $ envReader env }
instance (MonadBase IO m, Monoid w) => MonadWriter w (RWST r w s m) where
tell w = RWST $ ask >>= flip modifyIORef (<> w) . envWriter
listen (RWST (ReaderT f)) = RWST $ ReaderT $ \env -> do
iwriter <- newIORef mempty
result <- f env { envWriter = iwriter }
w <- readIORef iwriter
return (result, w)
pass (RWST (ReaderT f)) = RWST $ ReaderT $ \env -> do
(result, g) <- f env
modifyIORef (envWriter env) g
return result
instance MonadBase IO m => MonadState s (RWST r w s m) where
get = RWST $ ask >>= readIORef . envState
put s = RWST $ ask >>= flip writeIORef s . envState
instance (MonadBase IO m, Monoid w) => MonadRWS r w s (RWST r w s m)
runRWST :: (MonadBase IO m, Monoid w) => RWST r w s m a -> r -> s -> m (a, s, w)
runRWST (RWST (ReaderT f)) r s = do
iwriter <- newIORef mempty
istate <- newIORef s
a <- f $ Env r iwriter istate
w <- readIORef iwriter
s' <- readIORef istate
return (a, s', w)
instance MonadBase b m => MonadBase b (RWST r w s m) where
liftBase = lift . liftBase
instance MonadTransControl (RWST r w s) where
newtype StT (RWST r w s) a = StRWS {unStRWS :: a}
liftWith f = RWST $ ReaderT $ \r -> f $ \(RWST t) -> liftM StRWS $ runReaderT t r
restoreT = RWST . ReaderT . const . liftM unStRWS
instance MonadBaseControl b m => MonadBaseControl b (RWST r w s m) where
newtype StM (RWST r w s m) a = ST { unST :: ComposeSt (RWST r w s) m a }
liftBaseWith = defaultLiftBaseWith ST
restoreM = defaultRestoreM unST
main :: IO ()
main = do
res <- try $ runRWST
(step1 `finally` step2)
"This is the environment"
0
print (res :: Either ErrorCall ((), Int, [Bool]))
where
step1 = do
liftIO $ putStrLn $ "In step1"
modify (+ 1)
error "User exception"
step2 = do
s <- get
liftIO $ putStrLn $ "In step2: " ++ show s