*** 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.