|Peter J. Jones d354d773d9 Write a version of sizeT that uses type classes||5 years ago|
|img||5 years ago|
|src||5 years ago|
|vendor||5 years ago|
|.gitignore||5 years ago|
|.gitmodules||5 years ago|
|GNUmakefile||5 years ago|
|LICENSE||5 years ago|
|README.md||5 years ago|
|slides.md||5 years ago|
\ Creative Commons image by gynti
Most languages make a distinction between values that represent failure
(errors) and the mechanism to abort computations and unwind the stack
(exceptions.) Haskell is unique in that the type system makes it safe
and easy to build failure into types instead of lumping everything into
It also stands out by supporting exceptions through a library of functions and types instead of directly in the syntax of the language. The fact that there’s no dedicated keywords for exceptions might seem weird until you discover how flexible and expressive Haskell is.
This presentation aims to show how closely related errors and exceptions are, and how to keep them separate.
No dedicated syntax
Very limited in Haskell 2010
Expanded by GHC
In order to understand how exceptions work we first need to talk about type inhabitants and bottom.
Bool type is a very simple type that doesn’t use any type
variables and only has 2 data constructors. This means that there can
only be 2 unique values for this type. Or does it?
data Bool = False | True
All types in Haskell support a value called bottom. This means that the
Bool type actually has 3 possible values. Exceptions and
non-termination are examples of bottom values.
The list below illustrates that bottom values aren’t a problem until they’re evaluated.
bools :: [Bool] bools = [False, True, undefined]
Haskell includes 2 functions for creating bottom values:
error. In GHC
undefined is implemented using
throws an exception.
You can create a bottom value directly by writing a non-terminating function.
-- Raises exceptions in GHC: undefined :: a error :: String -> a -- Non-termination: badBoy :: a badBoy = badBoy
Catching exceptions is straight forward as long as you remember that you
can only catch exceptions in the
inline :: Int -> IO Int inline x = catch (shortFuse x) (\(_ex :: StupidException) -> return 0)
The second argument to
catch is a function to handle a caught
exception. GHC uses the type of the function to determine if it can
handle the caught exception. If GHC can’t infer the type of the function
you’ll need to add a type annotation like in the example above. This
If you want to handle more than one exception type you’ll need to use
something like the
catches function. To catch all possible exceptions
you can catch the
SomeException type since it’s at the top of the
exception type hierarchy. This isn’t generally wise and instead you
should use something like the
One interesting thing to note is that GHC differs from Haskell 2010 with
catch. Haskell 2010 states that
catch should catch all
exceptions regardless of their type. Probably because those exceptions
would all be
Below is another example of catching exceptions. This time a helper
function with an explicit type signature is used to handle the
exception. This allows us to avoid inline type annotations and the
helper :: Int -> IO Int helper x = catch (shortFuse x) handler where handler :: StupidException -> IO Int handler _ = return 0
Throwing exceptions is really easy, although you must be in the
monad to do so. Haskell 2010 provides a set of functions for creating
and raising exceptions.
-- Create an exception. userError :: String -> IOError -- Raise an exception. ioError :: IOError -> IO a -- fail from the IO Monad is both. fail = ioError . userError :: String -> IO a
GHC adds on to Haskell 2010 with functions like
throw function allows you to raise an exception in pure code and
is considered to be a misfeature.
shortFuse :: Int -> IO Int shortFuse x = if x > 0 then return (x - 1) else throwIO StupidException
As mentioned above, GHC adds a
throw function that allows you to raise
an exception from pure code. Unfortunately this makes it very difficult
naughtyFunction :: Int -> Int naughtyFunction x = if x > 0 then x - 1 else throw StupidException
You need to ensure that values are evaluated because they might contain unevaluated exceptions.
In the example below you’ll notice the use of the “
$!” operator. This
forces evaluation to WHNF so exceptions don’t sneak out of the
function as unevaluated thunks.
forced :: Int -> IO Int forced x = catch (return $! naughtyFunction x) (\(_ex :: StupidException) -> return 0)
Any type can be used as an exception as long as it’s an instance of the
Exception type class. Deriving from the
Typeable class makes
Exception instance trivial. However, using
means you need to enable the
DeriveDataTypeable GHC extension.
You can also automatically derive the
Show instance as with most other
types, but creating one manually allows you to write a more descriptive
message for the custom exception.
data StupidException = StupidException deriving (Typeable) instance Show StupidException where show StupidException = "StupidException: you did something stupid" instance Exception StupidException
Concurrency greatly complicates exception handling. The GHC runtime uses exceptions to send various signals to threads. You also need to be very careful with unevaluated thunks exiting from a thread when it terminates.
Additional problems created by concurrency:
Exceptions are used to kill threads
Exceptions are asynchronous
Need to mask exceptions in critical code
Probably don’t want unevaluated exceptions leaking out
Just use the async package.
Checked by the compiler
Way better than
Haskell is great about forcing programmers to deal with problems at compile time. That said, it’s still possible to write code which may not work at runtime. Especially with partial functions.
The function below will throw an exception at runtime if it’s given an
empty list. This is because
head is a partial function and only works
with non-empty lists.
stupid :: [Int] -> Int stupid xs = head xs + 1
Prefer errors to exceptions.
A better approach is to avoid the use of
head and pattern match the
list directly. The function below is total since it can handle lists
of any length (including infinite lists).
Of course, if the list or its head is bottom (⊥) then this function will throw an exception when the patterns are evaluated.
better :: [Int] -> Maybe Int better  = Nothing better (x:_) = Just (x + 1)
This is the version I like most because it reuses existing functions that are well tested.
listToMaybe function comes with the Haskell Platform. It takes a
list and returns its head in a
Just. If the list is empty it returns
Nothing. Alternatively you can use the
headMay function from the
reuse :: [Int] -> Maybe Int reuse = fmap (+1) . listToMaybe
Another popular type when dealing with failure is
Either which allows
you to return a value with an error. It’s common to include an error
message using the
Either it’s also common to define your own type
that indicates success or failure. We won’t discuss this further.
withError :: [Int] -> Either String Int withError  = Left "this is awkward" withError (x:_) = Right (x + 1)
Either are also monads!
If you have several functions that return one of these types you can use
do notation to sequence them and abort the entire block on the first
failure. This allows you to write short code that implicitly checks the
return value of every function.
Things tend to get a bit messy when you mix monads though…
The code below demonstrates mixing two monads,
we want to be able to perform I/O but we also want to use the
type to signal when a file doesn’t exist. This isn’t too complicated,
but what happens when we want to use the power of the
Maybe monad to
short circuit a computation when we encounter a
size :: FilePath -> IO (Maybe Integer) size f = do exist <- fileExist f if exist then Just <$> fileSize f else return Nothing
IO is the outer monad and we can’t do without it, we sort of
lose the superpowers of the
add :: FilePath -> FilePath -> IO (Maybe Integer) add f1 f2 = do s1 <- size f1 case s1 of Nothing -> return Nothing Just x -> size f2 >>= \s2 -> case s2 of Nothing -> return Nothing Just y -> return . Just $ x + y
MaybeT monad transformer we can make
IO the inner monad
and restore the
Maybe goodness. We don’t really see the benefit in the
sizeT function but note that its complexity remains about the same.
sizeT :: FilePath -> MaybeT IO Integer sizeT f = do exist <- lift (fileExist f) if exist then lift (fileSize f) else mzero
The real payoff comes in the
addT function. Compare with the
addT :: FilePath -> FilePath -> IO (Maybe Integer) addT f1 f2 = runMaybeT $ do s1 <- sizeT f1 s2 <- sizeT f2 return (s1 + s2)
This version using
Either is nearly identical to the
above. The only difference is that we can now report the name of the
file which doesn’t exist.
size :: FilePath -> IO (Either String Integer) size f = do exist <- fileExist f if exist then Right <$> fileSize f else return . Left $ "no such file: " ++ f
To truly abort the
add function when one of the files doesn’t exist
we’d need to replicate the nested
case code from the
Here I’m cheating and using
Either’s applicative instance. However,
this doesn’t short circuit the second file test if the first fails.
add :: FilePath -> FilePath -> IO (Either String Integer) add f1 f2 = do s1 <- size f1 s2 <- size f2 return ((+) <$> s1 <*> s2)
ErrorT monad transformer is to
MaybeT is to
Maybe. Again, changing
size to work with a transformer isn’t that
big of a deal.
sizeT :: FilePath -> ErrorT String IO Integer sizeT f = do exist <- lift $ fileExist f if exist then lift $ fileSize f else fail $ "no such file: " ++ f
But it makes a big difference in the
addT :: FilePath -> FilePath -> IO (Either String Integer) addT f1 f2 = runErrorT $ do s1 <- sizeT f1 s2 <- sizeT f2 return (s1 + s2)
The really interesting thing is that we didn’t actually have to change
size at all. We could have retained the non-transformer version and
ErrorT constructor to lift the
size function into the
MaybeT constructor can be used in a similar way.
addT' :: FilePath -> FilePath -> IO (Either String Integer) addT' f1 f2 = runErrorT $ do s1 <- ErrorT $ size f1 s2 <- ErrorT $ size f2 return (s1 + s2)
try function allows us to turn exceptions into errors in the form
Either, or as you now know,
It’s not hard to see how flexible exception handling in Haskell is, in no small part due to it not being part of the syntax. Non-strict evaluation is the other major ingredient.
try :: Exception e => IO a -> IO (Either e a) -- Which is equivalent to: try :: Exception e => IO a -> ErrorT e IO a
Prefer Errors to Exceptions!
Don’t Write/Use Partial Functions!