Implementing exceptions explicitly
The great thing about Haskell is that it is not necessary to "hard-wire" exception handling into the language. Everything is already there to implement the definition and handling of exceptions nicely.
A monadic exception type
First we implement exception handling for non-monadic functions. Since no I/O actions are involved, we still cannot handle exceptional situations induced from outside the world, but we can handle situations where it is unacceptable for the caller to check a priori whether the call can succeed.
data Exceptional e a =
Success a
| Exception e
deriving (Show)
instance Monad (Exceptional e) where
return = Success
Exception l >>= _ = Exception l
Success r >>= k = k r
throw :: e -> Exceptional e a
throw = Exception
catch :: Exceptional e a -> (e -> Exceptional e a) -> Exceptional e a
catch (Exception l) h = h l
catch (Success r) _ = Success rNow we extend this to monadic actions.
This is not restricted to I/O, but may be used immediately also for
non-deterministic algorithms implemented with the the monadic type
List a (or just [a] in Haskell 2010).
newtype ExceptionalT e m a =
ExceptionalT {runExceptionalT :: m (Exceptional e a)}
instance Monad m => Monad (ExceptionalT e m) where
return = ExceptionalT . return . Success
m >>= k = ExceptionalT $
runExceptionalT m >>= \ a ->
case a of
Exception e -> return (Exception e)
Success r -> runExceptionalT (k r)
throwT :: Monad m => e -> ExceptionalT e m a
throwT = ExceptionalT . return . Exception
catchT :: Monad m =>
ExceptionalT e m a -> (e -> ExceptionalT e m a) -> ExceptionalT e m a
catchT m h = ExceptionalT $
runExceptionalT m >>= \ a ->
case a of
Exception l -> runExceptionalT (h l)
Success r -> return (Success r)
bracketT :: Monad m =>
ExceptionalT e m h ->
(h -> ExceptionalT e m ()) ->
(h -> ExceptionalT e m a) ->
ExceptionalT e m a
bracketT open close body =
open >>= (\ h ->
ExceptionalT $
do a <- runExceptionalT (body h)
runExceptionalT (close h)
return a)Here are some examples for typical I/O actions with explicit exceptions.
data IOException =
DiskFull
| FileDoesNotExist
| ReadProtected
| WriteProtected
| NoSpaceOnDevice
deriving (Show, Eq, Enum)
open :: FilePath -> ExceptionalT IOException IO Handle
close :: Handle -> ExceptionalT IOException IO ()
read :: Handle -> ExceptionalT IOException IO String
write :: Handle -> String -> ExceptionalT IOException IO ()
readText :: FilePath -> ExceptionalT IOException IO String
readText fileName =
bracketT (open fileName) close $ \h ->
read hFinally we can escape from this particular monad-transformer type if we handle the exceptions completely.
main :: IO ()
main =
do result <- runExceptionalT (readText "test")
case result of
Exception e -> putStrLn ("When reading file 'test' we encountered exception " ++ show e)
Success x -> putStrLn ("Content of the file 'test'\n" ++ x)As a library
| Hackage | http://hackage.haskell.org/package/explicit-exception |
| Repository | darcs get http://code.haskell.org/explicit-exception/
|
Processing individual exceptions
So far I used the sum type IOException that subsumes a bunch of
exceptions. However, not all of these exceptions can be thrown by all of the
I/O actions. For example, something like a read function cannot throw
WriteProtected or NoSpaceOnDevice. Thus when
handling exceptions we do not want to handle WriteProtected if
we know that it cannot occur in the real world.
We like to express this in the type and actually we can express this in the type.
Consider two exceptions: ReadException and
WriteException. In order to be able to freely combine these
exceptions, we use type classes, since type constraints of two function or
action calls are automatically merged.
import Control.Monad.Exception.Synchronous (ExceptionalT)
class ThrowsRead e where throwRead :: e
class ThrowsWrite e where throwWrite :: e
readFile :: ThrowsRead e => FilePath -> ExceptionalT e IO String
writeFile :: ThrowsWrite e => FilePath -> String -> ExceptionalT e IO ()For example, in
copyFile src dst =
writeFile dst =<< readFile srcthe compiler automatically infers
copyFile ::
(ThrowsWrite e, ThrowsRead e) =>
FilePath -> FilePath -> ExceptionalT e IO ()Instead of ExceptionalT e m a you can also use
EitherT x m a. It's also simple to add parameters to
throwRead and throwWrite, such that you can
pass more precise information along with the exception. I just want to
keep it simple for now.
With those definitions you can already write a nice library and defer the decision of the particular exception types to the library user. The user might define something like
data ApplicationException =
ReadException
| WriteException
instance ThrowsRead ApplicationException where
throwRead = ReadException
instance ThrowsWrite ApplicationException where
throwWrite = WriteExceptionUsing ApplicationException however it is cumbersome to handle
only ReadException and propagate WriteException.
The user might write something like
case e of
ReadException -> handleReadException
WriteException -> throwT throwWritein order to handle a ReadException and regenerate a
ThrowWrite e => e type variable, instead of the concrete
ApplicationException type.
The user may choose to switch on multi-parameter type classes and overlapping
instances, define an exception type like data EE l and then reuse
the technique from control-monad-exception for exception handling
with the monadic ExceptionalT types.
Now I like to propose a technique for handling a particular set of exceptions in Haskell 98:
data ReadException e =
ReadException
| NoReadException e
instance ThrowsRead (ReadException e) where
throwRead = ReadException
instance ThrowsWrite e => ThrowsWrite (ReadException e) where
throwWrite = NoReadException throwWrite
data WriteException e =
WriteException
| NoWriteException e
instance ThrowsRead e => ThrowsRead (WriteException e) where
throwRead = NoWriteException throwRead
instance ThrowsWrite (WriteException e) where
throwWrite = WriteException
Defining exception types as a sum of "this particular exception" and
"another exception" lets us compose concrete types that can carry a
certain set of exceptions on the fly. This is very similar to switching
from particular monadic types to monad-transformer types. Thanks to the
type class approach the order of composition need not to be fixed by the
throwing function or action but is determined by the order of catching. We
even do not have to fix the nested exception type fully when catching an
exception. It is enough to fix the part that is interesting for
catch:
import Control.Monad.Exception.Synchronous (Exceptional(Success,Exception))
catchRead :: ReadException e -> Exceptional e String
catchRead ReadException = Success "catched a read exception"
catchRead (NoReadException e) = Exception e
throwReadWrite :: (ThrowsRead e, ThrowsWrite e) => e
throwReadWrite =
asTypeOf throwRead throwWrite
exampleCatchRead :: (ThrowsWrite e) => Exceptional e String
exampleCatchRead =
catchRead throwReadWriteNote how in exampleCatchRead the constraint
ThrowsRead e is removed from the constraint list of
throwReadWrite.
Unfortunately the library has to define instances for exceptions. Even worse, if your application imports package A and package B with their sets of exception types, you have to make the exception types of A instances of the exception classes of B and vice versa, and these would be orphan instances. Thus I propose that a library does not export any exception type, but only its exception classes. It can define exception types internally for catching exceptions itself. This way your application would define the exception types for the exceptions it wants to catch and define instances against all exception classes that occur in the called functions.
See also
- control-monad-exception (reduces the number of type class instances by some type extensions)