Exception
From HaskellWiki
(darcs repository) |
(→Processing individual exceptions: add explanations to the source code) |
||
| (9 intermediate revisions not shown.) | |||
| Line 5: | Line 5: | ||
Unfortunately Haskell's standard library names common exceptions of IO actions <hask>IOError</hask> | Unfortunately Haskell's standard library names common exceptions of IO actions <hask>IOError</hask> | ||
and the module <hask>Control.Monad.Error</hask> is about exception handling not error handling. | and the module <hask>Control.Monad.Error</hask> is about exception handling not error handling. | ||
| - | In general you should be very careful | + | In general you should be very careful not to [[Error vs. Exception|mix up]] exceptions with [[error]]s. |
Actually, an unhandled exception is an [[error]]. | Actually, an unhandled exception is an [[error]]. | ||
== Implementation == | == Implementation == | ||
| - | The great thing about Haskell is | + | === Exception monad === |
| - | Everything is already there to implement definition and handling of exceptions nicely. | + | |
| - | See the implementation in <hask>Control.Monad.Error</hask> (and please, excuse the misleading name | + | The great thing about Haskell is that it is not necessary to hard-wire the exception handling into the language. |
| + | Everything is already there to implement the definition and handling of exceptions nicely. | ||
| + | See the implementation in <hask>Control.Monad.Error</hask> (and please, excuse the misleading name for now). | ||
There is an old dispute between C++ programmers on whether exceptions or error return codes are the right way. | There is an old dispute between C++ programmers on whether exceptions or error return codes are the right way. | ||
Also Niklaus Wirth considered exceptions to be the reincarnation of GOTO and thus omitted them in his languages. | Also Niklaus Wirth considered exceptions to be the reincarnation of GOTO and thus omitted them in his languages. | ||
| - | + | Haskell solves the problem a diplomatic way: | |
| - | + | Functions return error codes, but the handling of error codes does not uglify the calling code. | |
First we implement exception handling for non-monadic functions. | First we implement exception handling for non-monadic functions. | ||
| - | Since no IO functions are involved, we | + | Since no IO functions are involved, we still cannot handle exceptional situations induced from outside the world, |
| - | but we can handle situations | + | but we can handle situations where it is unacceptable for the caller to check a priori whether the call can succeed. |
<haskell> | <haskell> | ||
| - | data | + | data Exceptional e a = |
Success a | Success a | ||
| Exception e | | Exception e | ||
deriving (Show) | deriving (Show) | ||
| - | instance Monad ( | + | instance Monad (Exceptional e) where |
return = Success | return = Success | ||
Exception l >>= _ = Exception l | Exception l >>= _ = Exception l | ||
Success r >>= k = k r | Success r >>= k = k r | ||
| - | throw :: e -> | + | throw :: e -> Exceptional e a |
throw = Exception | throw = Exception | ||
| - | catch :: | + | catch :: Exceptional e a -> (e -> Exceptional e a) -> Exceptional e a |
catch (Exception l) h = h l | catch (Exception l) h = h l | ||
catch (Success r) _ = Success r | catch (Success r) _ = Success r | ||
| Line 44: | Line 46: | ||
This is not restricted to IO, but may be used immediately also for non-deterministic algorithms implemented with the <hask>List</hask> monad. | This is not restricted to IO, but may be used immediately also for non-deterministic algorithms implemented with the <hask>List</hask> monad. | ||
<haskell> | <haskell> | ||
| - | newtype | + | newtype ExceptionalT e m a = |
| - | + | ExceptionalT {runExceptionalT :: m (Exceptional e a)} | |
| - | instance Monad m => Monad ( | + | instance Monad m => Monad (ExceptionalT e m) where |
| - | return = | + | return = ExceptionalT . return . Success |
| - | m >>= k = | + | m >>= k = ExceptionalT $ |
| - | + | runExceptionalT m >>= \ a -> | |
case a of | case a of | ||
Exception e -> return (Exception e) | Exception e -> return (Exception e) | ||
| - | Success r -> | + | Success r -> runExceptionalT (k r) |
| - | throwT :: Monad m => e -> | + | throwT :: Monad m => e -> ExceptionalT e m a |
| - | throwT = | + | throwT = ExceptionalT . return . Exception |
catchT :: Monad m => | catchT :: Monad m => | ||
| - | + | ExceptionalT e m a -> (e -> ExceptionalT e m a) -> ExceptionalT e m a | |
| - | catchT m h = | + | catchT m h = ExceptionalT $ |
| - | + | runExceptionalT m >>= \ a -> | |
case a of | case a of | ||
| - | Exception l -> | + | Exception l -> runExceptionalT (h l) |
Success r -> return (Success r) | Success r -> return (Success r) | ||
bracketT :: Monad m => | bracketT :: Monad m => | ||
| - | + | ExceptionalT e m h -> | |
| - | (h -> | + | (h -> ExceptionalT e m ()) -> |
| - | (h -> | + | (h -> ExceptionalT e m a) -> |
| - | + | ExceptionalT e m a | |
bracketT open close body = | bracketT open close body = | ||
open >>= (\ h -> | open >>= (\ h -> | ||
| - | + | ExceptionalT $ | |
| - | do a <- | + | do a <- runExceptionalT (body h) |
| - | + | runExceptionalT (close h) | |
return a) | return a) | ||
</haskell> | </haskell> | ||
| Line 90: | Line 92: | ||
deriving (Show, Eq, Enum) | deriving (Show, Eq, Enum) | ||
| - | open :: FilePath -> | + | open :: FilePath -> ExceptionalT IOException IO Handle |
| - | close :: Handle -> | + | close :: Handle -> ExceptionalT IOException IO () |
| - | read :: Handle -> | + | read :: Handle -> ExceptionalT IOException IO String |
| - | write :: Handle -> String -> | + | write :: Handle -> String -> ExceptionalT IOException IO () |
| - | readText :: FilePath -> | + | readText :: FilePath -> ExceptionalT IOException IO String |
readText fileName = | readText fileName = | ||
bracketT (open fileName) close $ \h -> | bracketT (open fileName) close $ \h -> | ||
| Line 108: | Line 110: | ||
main :: IO () | main :: IO () | ||
main = | main = | ||
| - | do result <- | + | do result <- runExceptionalT (readText "test") |
case result of | case result of | ||
Exception e -> putStrLn ("When reading file 'test' we encountered exception " ++ show e) | Exception e -> putStrLn ("When reading file 'test' we encountered exception " ++ show e) | ||
| Line 114: | Line 116: | ||
</haskell> | </haskell> | ||
| + | |||
| + | {{PackageInfoBox|name=explicit-exception|darcs-code=explicit-exception/}} | ||
| + | |||
| + | === Processing individual exceptions === | ||
| + | |||
| + | So far I used the sum type <hask>IOException</hask> that subsumes a bunch of exceptions. | ||
| + | However, not all of these exceptions can be thrown by all of the IO functions. E.g. a read function cannot throw <hask>WriteProtected</hask> or <hask>NoSpaceOnDevice</hask>. | ||
| + | Thus when handling exceptions we do not want to handle <hask>WriteProtected</hask> 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: <hask>ReadException</hask> and <hask>WriteException</hask>. In order to be | ||
| + | able to freely combine these exceptions, we use type classes, since type | ||
| + | constraints of two function calls are automatically merged. | ||
| + | |||
| + | <haskell> | ||
| + | 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 () | ||
| + | </haskell> | ||
| + | |||
| + | |||
| + | For example for | ||
| + | |||
| + | <haskell> | ||
| + | copyFile src dst = | ||
| + | writeFile dst =<< readFile src | ||
| + | </haskell> | ||
| + | |||
| + | the compiler automatically infers | ||
| + | |||
| + | <haskell> | ||
| + | copyFile :: | ||
| + | (ThrowsWrite e, ThrowsRead e) => | ||
| + | FilePath -> FilePath -> ExceptionalT e IO () | ||
| + | </haskell> | ||
| + | |||
| + | |||
| + | Instead of <hask>ExceptionalT</hask> you can also use <hask>EitherT</hask> or <hask>ErrorT</hask>. | ||
| + | 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 | ||
| + | |||
| + | <haskell> | ||
| + | data ApplicationException = | ||
| + | ReadException | ||
| + | | WriteException | ||
| + | |||
| + | instance ThrowsRead ApplicationException where | ||
| + | throwRead = ReadException | ||
| + | |||
| + | instance ThrowsWrite ApplicationException where | ||
| + | throwWrite = WriteException | ||
| + | </haskell> | ||
| + | |||
| + | Using <hask>ApplicationException</hask> however it is cumbersome to handle only <hask>ReadException</hask> and propagate <hask>WriteException</hask>. | ||
| + | The user might write something like | ||
| + | |||
| + | <haskell> | ||
| + | case e of | ||
| + | ReadException -> handleReadException | ||
| + | WriteException -> throwT throwWrite | ||
| + | </haskell> | ||
| + | |||
| + | in order to handle a <hask>ReadException</hask> and regenerate a <hask>ThrowWrite e => e</hask> type variable, instead of the concrete <hask>ApplicationException</hask> type. | ||
| + | |||
| + | He may choose to switch on multi-parameter type classes and overlapping | ||
| + | instances, define an exception type like <hask>data EE l</hask> and then use the technique from <code>control-monad-exception</code> for exception handling with the <hask>ExceptionalT</hask> monads. | ||
| + | |||
| + | Now I like to propose a technique for handling a particular set of | ||
| + | exceptions in Haskell 98: | ||
| + | |||
| + | <haskell> | ||
| + | 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 | ||
| + | </haskell> | ||
| + | |||
| + | |||
| + | 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 monads to monad transformers. Thanks to the type class | ||
| + | approach the order of composition needs not to be fixed by the throwing | ||
| + | function 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 <hask>catch</hask>: | ||
| + | |||
| + | <haskell> | ||
| + | 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 throwReadWrite | ||
| + | </haskell> | ||
| + | |||
| + | Note how in <hask>exampleCatchRead</hask> the constraint <hask>ThrowsRead</hask> is removed from the constraint list of <hask>throwReadWrite</hask>. | ||
| + | |||
| + | The nasty thing is, that the library has to define <math>n^2</math> instances for <math>n</math> 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 are 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 == | == See also == | ||
* [[Error]] | * [[Error]] | ||
| - | * | + | * [[Error vs. Exception]] |
| + | * {{HackagePackage|id=control-monad-exception}} (reduces the number of type class instances by some type extensions) | ||
[[Category:Glossary]] | [[Category:Glossary]] | ||
Current revision
An exception denotes an unpredictable situation at runtime, like "out of disk storage", "read protected file", "user removed disk while reading", "syntax error in user input". These are situation which occur relatively seldom and thus their immediate handling would clutter the code which should describe the regular processing. Since exceptions must be expected at runtime there are also mechanisms for (selectively) handling them.
(In general you should be very careful not to mix up exceptions with errors. Actually, an unhandled exception is an error.
Contents |
1 Implementation
1.1 Exception monad
The great thing about Haskell is that it is not necessary to hard-wire the exception handling into the language. Everything is already there to implement the definition and handling of exceptions nicely.
See the implementation inThere is an old dispute between C++ programmers on whether exceptions or error return codes are the right way. Also Niklaus Wirth considered exceptions to be the reincarnation of GOTO and thus omitted them in his languages. Haskell solves the problem a diplomatic way: Functions return error codes, but the handling of error codes does not uglify the calling code.
First we implement exception handling for non-monadic functions. Since no IO functions 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 r
Now we extend this to monadic functions.
This is not restricted to IO, but may be used immediately also for non-deterministic algorithms implemented with thenewtype 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 IO functions 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 h
Finally we can escape from the Exception monad 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)
| Hackage | http://hackage.haskell.org/package/explicit-exception |
| Repository | darcs get http://code.haskell.org/explicit-exception/
|
1.2 Processing individual exceptions
So far I used the sum typeWe like to express this in the type and actually we can express this in the type.
Consider two exceptions:able to freely combine these exceptions, we use type classes, since type constraints of two function 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 for
copyFile src dst = writeFile dst =<< readFile src
the compiler automatically infers
copyFile :: (ThrowsWrite e, ThrowsRead e) => FilePath -> FilePath -> ExceptionalT e IO ()
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 = WriteException
The user might write something like
case e of ReadException -> handleReadException WriteException -> throwT throwWrite
He may choose to switch on multi-parameter type classes and overlapping
instances, define an exception type likecontrol-monad-exception for exception handling with the 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 monads to monad transformers. Thanks to the type class
approach the order of composition needs not to be fixed by the throwing
function 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
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 throwReadWrite
The nasty thing is, that the library has to define n2 instances for n 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 are 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.
2 See also
- Error
- Error vs. Exception
- control-monad-exception (reduces the number of type class instances by some type extensions)
