*** Exception: Prelude.head: empty list
In Error versus Exception, Henning Thielemann makes a clear
distinction between errors and exceptions in Haskell. Even
though not all Haskellers make this distinction, it's useful to do so
in order to talk about the most basic ways to handle computations that
can go wrong and to discuss unsafe functions such as head
,
fromJust
, and (!!)
.
On the one hand, an error is a programming mistake such as a division
by zero, the head of an empty list, or a negative index. If we
identify an error, we remove it. Thus, we don't handle errors, we
simply fix them. In Haskell, we have error
and undefined
to cause
such errors and terminate execution of the program.
On the other hand, an exception is something that can go wrong or
that simply doesn't follow a rule but can happen. For example, reading
a file that does not exist or that is not readable. If we identify a
possible exception, we handle it, and not doing so would be an
error. In Haskell, we have pure (Maybe
and Either
, for instance)
and impure ways to handle exceptions.
A basic example of an error in Haskell is trying to get the head of an
empty list using the head
function as defined in GHC.List
:
head :: [a] -> a
head [] = error "head: empty list"
head (x:_) = x
One way to distinguish an error from an exception is to think in terms
of contracts and preconditions. In this case, there's a precondition
in the documentation of the head
function: the list must be
nonempty. This means that the first equation of head
is supposed to
be dead or unreachable code. This way, if we are sure that a list has
at least one element, we can extract its head:
ghci> head [104,97,115,107,101,108,108]
104
Of course, the type signature of the head
function says nothing
about such contract, which means that there's nothing stopping us from
applying it to an empty list and therefore breaking the rules:
ghci> head []
*** Exception: head: empty list
As a comment in the definition of the fromJust
function in the
Data.Maybe
module says, "yuck."
Even if trying to get the head of an empty list using head
is an
error, it's unsafe to do so. We can certainly treat it as an exception
and handle it with the Maybe
data type:
data Maybe a = Nothing | Just a
In terms of exceptions, the Maybe
type represents a computation that
can fail (in the case of a Nothing
).
Let's define a safe version of the head
function:
maybeHead :: [a] -> Maybe a
maybeHead [] = Nothing
maybeHead (x:_) = Just x
The safety of the maybeHead
function relies on its type
signature. We know that applying the function to a list can succeed:
ghci> maybeHead [104,97,115,107,101,108,108]
Just 104
Or fail:
ghci> maybeHead []
Nothing
A similar example is the fromJust
function, which
extracts the element out of a Just
:
fromJust :: Maybe a -> a
fromJust Nothing = error "Maybe.fromJust: Nothing" -- yuck
fromJust (Just x) = x
Again, it's an error to apply fromJust
to a Nothing
, but there's
nothing stopping us from doing it. It's best if we use a safe function
such as fromMaybe
, which takes a default value:
fromMaybe :: a -> Maybe a -> a
fromMaybe d mx =
case mx of
Nothing -> d
Just x -> x
Yet another example is the lookup
function, which looks up a key in
an association list or dictionary:
lookup :: Eq a => a -> [(a,b)] -> Maybe b
lookup _ [] = Nothing
lookup key ((x,y):xys)
| key == x = Just y
| otherwise = lookup key xys
In this case, applying lookup
to an empty list or to a list which
doesn't contain the key we're looking for is not an error but an
exception, and the type signature of the function clearly specifies
that it can go wrong:
ghci> lookup 1 (zip [1..] [104,97,115,107,101,108,108])
Just 104
ghci> lookup 1 []
Nothing
ghci> lookup (-1) (zip [1..] [104,97,115,107,101,108,108])
Nothing
Now, let's consider the elemAt
(or (!!)
) function, which is a list
index operator:
elemAt :: [a] -> Int -> a
elemAt xs n | n < 0 = error "elemAt: negative index"
elemAt [] _ = error "elemAt: index too large"
elemAt (x:_) 0 = x
elemAt (_:xs) n = elemAt xs (n - 1)
This function has two preconditions: the index must be nonnegative and less than the length of the list. For example:
ghci> elemAt [104,97,115,107,101,108,108] 0
104
ghci> elemAt [104,97,115,107,101,108,108] (-8)
*** Exception: elemAt: negative index
ghci> elemAt [104,97,115,107,101,108,108] 8
*** Exception: elemAt: index too large
The elemAt
function is as unsafe as head
and fromJust
in that
its type signature tells us nothing about the possibility of
failure. We could define a safe version using Maybe
, but now we have
two different errors and it would be nice to provide additional
information about what went wrong, which we can accomplish with the
Either
data type:
data Either a b = Left a | Right b
In terms of exceptions, a Left
represents failure and a Right
represents success.
Here's a safe version of elemAt
using strings for exceptions:
eitherElemAt :: [a] -> Int -> Either String a
eitherElemAt _ n | n < 0 = Left "elemAt: negative index"
eitherElemAt [] _ = Left "elemAt: index too large"
eitherElemAt (x:_) 0 = Right x
eitherElemAt (_:xs) n = eitherElemAt xs (n - 1)
We can safely apply this version of elemAt
to the lists and indexes
we used before:
ghci> eitherElemAt [104,97,115,107,101,108,108] 0
Right 104
ghci> eitherElemAt [104,97,115,107,101,108,108] 8
Left "elemAt: index too large"
ghci> eitherElemAt [104,97,115,107,101,108,108] (-8)
Left "elemAt: negative index"
We know that there are only two things that can go wrong with elemAt
and that means that a String
is too general for representing failure
in this case. We can be more specific by defining our own data type
and moving the error strings to the Show
instance:
data ElemAtError
= IndexTooLarge
| NegativeIndex
instance Show ElemAtError where
show IndexTooLarge = "elemAt: index too large"
show NegativeIndex = "elemAt: negative index"
And we can use this data type for exceptions in another safe version
of elemAt
:
errorElemAt :: [a] -> Int -> Either ElemAtError a
errorElemAt _ n | n < 0 = Left NegativeIndex
errorElemAt [] _ = Left IndexTooLarge
errorElemAt (x:_) 0 = Right x
errorElemAt (_:xs) n = errorElemAt xs (n - 1)
Which we can safely apply to the same lists and indexes as before:
ghci> errorElemAt [104,97,115,107,101,108,108] 0
Right 104
ghci> errorElemAt [104,97,115,107,101,108,108] 8
Left elemAt: index too large
ghci> errorElemAt [104,97,115,107,101,108,108] (-8)
Left elemAt: negative index
It might be confusing to call our custom data type ElemAtError
instead of ElemAtException
, but perhaps it's a better name for
reflecting that we're treating errors as exceptions for the sake of
safety.
It's even more confusing once we figure out that the implementation of
error
is actually raising an exception, but a very general one. Even
then, we can be more specific about the exception that gets thrown by
making our ElemAtError
type an instance of Exception
, as follows:
instance Exception ElemAtError
Instead of calling error
, we can now throw the constructors of our
ElemAtError
data type if there's a problem with the index:
exceptionElemAt :: [a] -> Int -> a
exceptionElemAt _ n | n < 0 = throw NegativeIndex
exceptionElemAt [] _ = throw IndexTooLarge
exceptionElemAt (x:_) 0 = x
exceptionElemAt (_:xs) n = exceptionElemAt xs (n - 1)
Which is very similar to what we had with the original elemAt
function:
ghci> exceptionElemAt [104,97,115,107,101,108,108] 0
104
ghci> exceptionElemAt [104,97,115,107,101,108,108] 8
*** Exception: elemAt: index too large
ghci> exceptionElemAt [104,97,115,107,101,108,108] (-8)
*** Exception: elemAt: negative index
But this time we can use the try
function from Control.Exception
,
which takes an action and returns either the result of that action or
an exception:
try :: Exception e => IO a -> IO (Either e a)
Since we're using a very specific type to represent things that can go
wrong with the elemAt
function, we can also be very specific about
what to do in case that something actually goes wrong:
tryExceptionElemAt :: Show a => [a] -> Int -> IO ()
tryExceptionElemAt xs n = do
eitherExceptionElemAt <- try (evaluate (exceptionElemAt xs n))
case eitherExceptionElemAt of
Left IndexTooLarge -> print IndexTooLarge
Left NegativeIndex -> print NegativeIndex
Right elemAt -> print elemAt
Given a list xs
and an index n
, we try to get the element at that
position using exceptionElemAt
, and then use a case expression to
pattern match against the Either
returned by try
. In this case,
we're simply printing the error or the result, which is not very
useful.
For now, we can try our lists and indexes, and see that we succesfully handled everything that could go wrong:
ghci> tryExceptionElemAt [104,97,115,107,101,108,108] 0
104
ghci> tryExceptionElemAt [104,97,115,107,101,108,108] 8
elemAt: index too large
ghci> tryExceptionElemAt [104,97,115,107,101,108,108] (-8)
elemAt: negative index
It's obviously better to use a safe function such as eitherElemAt
or
errorElemAt
, but exceptionElemAt
gives us a good idea of how to
raise and catch exceptions in Haskell.
Finally, let's consider reading a file using the readFile
function,
which could fail for two reasons: the file doesn't exist or the user
doesn't have enough permissions to read it. We'll use the tryJust
function, which is like try
but takes a handler that allows us to
select which exceptions are caught:
tryJust :: Exception e => (e -> Maybe b) -> IO a -> IO (Either b a)
Here's a function that tries to read a given file:
tryJustReadFile :: FilePath -> IO ()
tryJustReadFile filePath = do
eitherExceptionFile <- tryJust handleReadFile (readFile filePath)
case eitherExceptionFile of
Left er -> putStrLn er
Right file -> putStrLn file
where
handleReadFile :: IOError -> Maybe String
handleReadFile er
| isDoesNotExistError er = Just "readFile: does not exist"
| isPermissionError er = Just "readFile: permission denied"
| otherwise = Nothing
Given a file name, we try to read it with readFile
and choose the
exceptions we're going to handle with the handleReadFile
function. If the result of trying to read the file is a Left
, we
print the exception message. If it's a Right
, we print the contents
of the file. The handleReadFile
function returns appropriate
messages for errors that satisfy ioDoesNotExistError
or
isPermissionError
(which are exceptions in System.IO.Error
), and
ignores any other exception.
Let's try to read the contents of a file called haskell
before
creating it:
ghci> tryJustReadFile "haskell"
readFile: does not exist
We don't get an *** Exception
because we handled the exception and
decided to simply print the exception message.
If we create the file and add something to it, and then try to read its contents, we get the expected result:
$ echo [104,97,115,107,101,108,108] > haskell
ghci> tryJustReadFile "haskell"
[104,97,115,107,101,108,108]
And if we don't have permissions to read the file, we get the expected exception message:
$ chmod -r haskell
ghci> tryJustReadFile "haskell"
readFile: permission denied
For more information about errors and exceptions in Haskell, see the
Error Handling chapter in Real World Haskell or the
Control.Exception
module in the base
package.