© Stefania Loredana Nita and Marius Mihailescu 2017

Stefania Loredana Nita and Marius Mihailescu, Practical Concurrent Haskell, https://doi.org/10.1007/978-1-4842-2781-7_5

5. Exceptions

Stefania Loredana Nita and Marius Mihailescu1

(1)Bucharest, Romania

In any programming language, error handling is an important task. In general, when we talk about an unexpected behavior in our program, we refer to two terms: errors and exceptions. We need to distinguish between them to make the appropriate decision for our program.

The term exception is newer to programming than the error. An exception represents an anticipated, but improper case at runtime; whereas an error represents an inaccuracy, which can only be solved by changing the program.

The following are types of errors:

  • Syntax error. Occurs because there is a “spelling” mistake in the programming language

  • Semantic error. Occurs because the statements are used incorrectly

  • Logical error. Occurs because some specifications are not respected

The following are time errors:

  • Compile-time errors. Syntax errors and static semantic errors that are identified by the compiler

  • Runtime errors. Dynamic semantic errors and logical errors that are not identified by the compiler

Errors

The following are the error and exception functions in Haskell programming:

  • Errors. error, assert, Control.Exception. catch, Debug.Trace.trace

  • Exceptions. Prelude.catch, Control.Exception.catch, Control.Exception.try, IOError, Control.Monad.Error

Observe that the keyword catch belongs in both categories. But take a deeper look: they are different because Prelude.catch manages only exceptions; whereas Control.Exception.catch is used to catch types of unspecified values (such as undefined, error, and many other things).

Using the error Function

The error function, which belongs to Prelude, stops the execution of a program and returns an error message with custom text. undefined is a particular type of error with standard text. In Haskell, there is no difference between an undefined value and an infinite loop, in practice. Broadly, error and undefined are the same.

let a = a + 2 in a :: Int

The simplest ways to throw, generate, or indicate an error condition is with the error function. The following is a function that computes the nth number of the Fibonacci series .

fibo 0 = 1
fibo 1 = 1
fibo n = fibo(n-1) + fibo(n-2)


Prelude> fibo 3
3

What happens if the input is less than zero?

Prelude> fibo (-1)

*** Exception: <interactive>: Non-exhaustive patterns in function fibo

To avoid this case, we could complete our function as follows.

  fibo n | n < 0 = error "The argument should be positive!"
  fibo 0 = 1
  fibo 1 = 1
  fibo n = fibo(n-1) + fibo(n-2)

In practice, the error function is not used in error handling; instead, Maybe or Either are used, as presented in the next two sections.

Maybe

Let’s start with a simple example. We have a list of integers and we want to divide each by a number.

Prelude> divide x = map (x `div`)
divide :: Integral b => b -> [b] -> [b]
Prelude> divide 100 [2, 10, 20, 50]
[50,10,5,2]

The preceding function worked as we wanted. We can test it on an infinite list; the result is as follows.

Prelude> take 10 (divide 100 [1..])
[100,50,33,25,20,16,14,12,11,10]

But the division has a special case: division with a zero. Let’s see what happens.

Prelude> take 10 (divide 100 [0..])
[*** Exception: divide by zero
Prelude> divide 100 [2, 10, 0, 20, 50]
[50,10,Prelude> *** Exception: divide by zero

In the first example, we get the divide by zero error because our list begins with a zero. In the second example, you can see how lazy initialization is applied: the allowed divisions are computed, and when 0 becomes the nominator, we get the divide by zero error, and the program stops.

How do we avoid situations like the preceding?

Let’s use Maybe, Nothing, or Just.

divide :: Integral x => x -> [x] -> Maybe [x]
divide _ [] = Just []
divide _ (0:_) = Nothing
divide a (b:ys) =
    case divide a ys of
      Nothing -> Nothing
      Just res -> Just ((a `div` b) : res)

The use of Maybe is the most common method to show that an error occurred. Another way is to return Nothing, if the zero element belongs to the list. There is also Just if 0 is not an element of the list.

Prelude> divide 100 [2, 10, 0, 20, 50]
Nothing
Prelude> divide 100 [2, 10, 20, 50]
Just [50,10,5,2]
Prelude> divide 100 [1..]
*** Exception: stack overflow

The first two examples work perfectly, but we get an error if the list is infinite. This is due to the use of Maybe. Because the result is Maybe [x], the whole list is traversed to determine if it contains the 0 element. Also, before compute the current outcome, in every stage of divide, the former outcomes need to be known. Therefore, an infinite list is not accepted as input in the error-handling version of the divide function.

In most programs, we know the special cases in which it would crash. Let’s think about two simple functions in lists: head and tail. If the input list is empty list, then we will get errors in both cases.

Prelude> head []
*** Exception: Prelude.head: empty list
Prelude> tail []
*** Exception: Prelude.tail: empty list

Let’s write our own head and tail functions , in which we handle the empty list case. Let’s begin with the function for tail.

tailEmptyHandling :: [x] -> Maybe [x]
tailEmptyHandling [] = Nothing
tailEmptyHandling (_:ys) = Just ys

When we use our function, we get the following.

Prelude> tailEmptyHandling []
Nothing
Prelude> tailEmptyHandling [1,2,3]
Just [2,3]

If we want the tail of an infinite list, we could proceed as follows, where we actually get only the first 10 elements of an infinite list.

Prelude> case tailEmptyHandling [1..] of {Nothing -> Nothing; Just a -> Just (take 10 a)}
Just [2,3,4,5,6,7,8,9,10,11]

Next, let’s see what the function for head looks like.

headEmptyHandling :: [x] -> Maybe x
headEmptyHandling [] = Nothing
headEmptyHandling (y:_) = Just y

These are the results.

Prelude> headEmptyHandling []
Nothing
Prelude> headEmptyHandling [1,2,3]
Just 1

So, you have seen how to handle an empty list or an infinite list for a head and tail function. Now, let’s return to our previous example and remember that, if the list contains a 0 value, no matter the index, the result will always be Nothing. But we want Nothing to be returned only for the 0 values, and the result of the division to be returned for the rest of the elements. We could proceed as follows.

divide :: Integral x => x -> [x] -> [Maybe x]
divide divident divisor =
    map ok divisor
    where ok 0 = Nothing
          ok x = Just (divident `div` x)

The following are the results.

Prelude> divide 100 [2, 10, 20, 50]
[Just 50,Just 10,Just 5,Just 2]
Prelude> divide 100 [2, 10, 20, 0, 50]
[Just 50,Just 10,Just 5,Nothing,Just 2]

The fact that we used divide :: Integral x => x -> [x] -> [Maybe x], instead of divide :: Integral x => x -> [x] -> Maybe [x] helps to maintain the laziness. An important benefit is that you can see exactly where the special case of division occurred.

A useful library is safe, that could be found at this web page. It is useful because it works on Data.List and throws exceptions in different situations. For every unsafe function, there are more versions of it. For example, tail has the following versions.

  • tail :: [a] -> [a]: The error occur on tail []

  • tailMay :: [a] -> Maybe [a]: Transforms errors in Nothing

  • tailDef :: [a] -> [a] -> [a]: Default to return on errors

  • tailNote :: String -> [a] -> [a]: Could be used for a particular error message

  • tailSafe :: [a] -> [a]: Returns a sensible default if possible; [] in the case of tail

Either

The use of Either is similar to the use of Maybe, but there is an important difference: Either can bind connected information for both a failure and a success. A function that returns Either has two sides: the right/correct value is returned with Right, and the wrong/incorrect value is returned with Left. The association between Right/Left and success/failure is not restricted. You can reversely bind them, but the convention is that the Right side is linked to success and the Left side is linked to failure.

The following shows the divide function written using Either.

divide :: Integral x => x -> [x] -> Either String [x]
divide _ [] = Right []
divide _ (0:_) = Left "divide: found 0"
divide divident (divisor:ys) =
    case divide divident ys of
      Left y -> Left y
      Right outputs -> Right ((divident `div` divisor) : outputs)

These are the results.

Prelude> divide 100 [2, 10, 20, 50]
Right [50,10,5,2]
Prelude> divide 100 [2, 10, 0, 20, 50]
Left "divide: found 0"

The implementation of Either is similar to the implementation of Maybe. Here, Right is corresponding to Just, and Left is corresponding to Nothing. An important feature of Either is that you can write your own failure message. The following example is modified so that there are restrictions on dividing by 2.

data DivisionError x = DivisionZero
                 | IllegalDivisor x
                   deriving (Eq, Read, Show)


divide :: Integral x => x -> [x] -> Either (DivisionError x) [x]
divide _ [] = Right []
divide _ (0:_) = Left DivisionZero
divide _ (2:_) = Left (IllegalDivisor 2)
divide divident (divisor:ys) =
    case divide divident ys of
      Left y -> Left y
      Right outcomes -> Right ((divident `div` divisor) : outcomes)

The following shows the results of calling our function.

Prelude> divide 100 [2, 10, 0, 20, 50]
Left (IllegalDivisor 2)
Prelude> divide 100 [12, 10, 0, 20, 50]
Left DivisionZero
Prelude> divide 100 [12, 10, 20, 50]
Right [8,10,5,2]

Exceptions

As in many programming languages, Haskell allows you to handle exceptions. Only in the IO monad does Haskell catch exceptions, because the order of the evaluation is not specified. A special syntax is not necessary because the techniques through which exceptions are caught are mostly functions.

Control.Exception is an important module that works with exceptions. In this module, different functions exist. Many types are defined to handle exceptions. Every exception that occurs has the Exception type, because it is the main type.

try is a common function that handles exceptions. It has two sides: Left – returns an exception, and Right – returns the output, if the program was run successfully. Let’s look at the following example.

Prelude> :m Control.Exception
Prelude > let a = 2 `div` 2
Prelude > let b = 2 `div` 0
Prelude > print a
1
Prelude > print b
*** Exception: divide by zero
Prelude > try (print a) :: IO (Either SomeException ())
1
Right ()
Prelude > try (print b)
Left divide by zero

First, we just call the print function without handling a possible exception, but then we use the try function to catch possible exceptions that occur in calling print. The exception was thrown when we called the print function, not when we defined b. When an expected situation occurs—namely, printing a valid result, two lines are displayed: the first is caused by print, which provides result 1, and the second is caused by GHCI, which says that the function was called successfully, without exceptions.

Lazy Evaluation and Exceptions

Let’s run the following program.

Prelude> let c = undefined
Prelude > try (print c)
Left Prelude.undefined
Prelude > outcome <- try (return c)
Right *** Exception: Prelude.undefined

As, you can see it is not a problem to assign the undefined value to c, but an exception occurs if you try to print it. But, why is there Right *** Exception: Prelude.undefined, if there is an exception? This is because Right comes from assignation, which works fine, but when it tries to print undefined, it gets an exception.

To avoid this kind of situations, use evaluate.

Prelude > let c = undefined
Prelude > outcome <- try (evaluate c)
Left Prelude.undefined
Prelude > outcome <- try (evaluate b)
Left divide by zero

evaluate is similar to return, but the system is forced to analyze the input .

The handle Function

Most times, we want our program to work in the case of success. But other times, we want it to work in case of failure; for example, we want to display a message. For that, we use the handle function , which has two sides: the second side is the function we want to call, and the first side is called in case of failure.

Prelude> :m Control.Exception
Prelude > let a = 2 `div` 2
Prelude > let b = 2 `div` 0
Prelude > handle (\_ -> putStrLn " Divide by zero ") (print a)
1
Prelude > handle (\_ -> putStrLn "Divide by zero") (print b)
Divide by zero

In the preceding example, we used a kind of brute force, because for any exception that arises, the message will be the same. Instead, you could use handleJust, which permits you to use particular types of exceptions, when the elements are described by a single type exception. For other types of exceptions, like arithmetic exceptions, I/O exceptions, custom exception, and so forth, a more powerful mechanism, such as Catch, is needed.

import Control.Exception

exeptionCatch:: Exception -> Maybe ()
exeptionCatch (ArithException IllegalDivision) = Just ()
exeptionCatch _ = Nothing


handle :: () -> IO ()
handle _ = putStrLn "We have an illegal operation: 0 as divisor."


myPrint :: Integer -> IO ()
myPrint a = handleJust exceptionCatch handle (print a)

exceptionCatch represents a function that is establish if the targeted exception occurs, returning Just(), which goes to the handle function, or otherwise, Nothing.

Prelude> let a = 2 `div` 2
Prelude > let b = 2 `div` 0
Prelude > myPrint a
1
Prelude > myPrint b
We have an illegal operation: 0 as divisor.

Input/Output Exceptions

In most cases, exceptions occur because of the input/output. For that, Haskell has a special module called System.IO.Error, which defines the main functions try and catch. If the exceptions that occur do not have an IOError type, then they will not be captured.

Note

System.IO.Error and Control.Exception have the same functions, but they act differently. You need to import these modules carefully. If both of them are imported, then there is an error when you use their functions, because the callings are ambiguous. So, you need to import them qualified, or conceal the symbols of one from the other. Another important aspect to remember is that the default catch used by Prelude is the System.IO.Error version.

The throw Function

Another aspect of exception handling is throw. In the earlier examples in this chapter, the exceptions were thrown by the system, but we can throw them on our own. The most used functions for this purpose are throw, throwIO, and ioError:

  • throw belongs to Control.Exception and it could generate any Exception

  • throwIO belongs to Control.Exception, but it generates exceptions (of type Exception) just for IO monad

  • ioError belongs to Control.Exception and System.Error.IO, and it is used to engender exceptions associated with I/O

Dynamic Exceptions

Data.Dynamic and Data.Typeable modules are used for dynamic exceptions. They are very useful, especially when working with databases, because they return the errors from SQL queries. Usually, SQL exceptions have three elements: a number representing the error code, a state, and a message. In this section, we will implement a function that simulates SQL errors.

{-# LANGUAGE DeriveDataTypeable #-}
import Data.Dynamic
import Control.Exception
data SqlException = SqlException {state :: String,
                          exceptionCode :: Int,
                          exceptionMessage :: String}
                deriving (Eq, Show, Read, Typeable)

In the last line of code, the data type becomes accessible for dynamic type, derived from Typeable. The first line is used to engender an object of type Typeable.

The following defines functions that implement catch and handle SQL errors. Observe that Haskell’s catch and handle are not able to figure out the error, because it does not fall under the Exception type class.

{- | It is executed the specified IO command.
When the SqlException occurs, it is executed the given
handler which returns its result. When our exception does
not occur, operate normally.-}
catchSqlEception :: IO x -> (SqlException -> IO x) -> IO x
catchSqlEception =  catchDyn


{- | This is the same as catchSql, but the arguments are reverted. -}
handleSqlException :: (SqlError -> IO x) -> IO x -> IO x
handleSqlException = flip catchSql

catchDyn is restricted to catch only SqlException.

If the program throws an exception, but it is not caught by any function, it will display a common error message; but when it comes to dynamic exceptions, which are unknown to the system, the message will not be clear. Instead, we could add a feature to our program so that all exceptions can be caught.

{- | There are caught SqlException and, then they are re-raised
Like IO errors and with failure. -}
featureHandleSqlException :: IO x -> IO x
featureHandleSqlException activity =
    catchSql activity myHandler
    where myHandler ex = fail ("Sql exception occured " ++ show ex)

The following throws a SqlException exception .

throwSqlException :: String -> Int -> String -> a
throwSqlException state exceptionCode exceptionMessage =
    throwDyn(SqlException state exceptionCode exceptionMessage)


throwSqlExceptionIO :: String -> Int -> String -> IO x
throwSqlExceptionIO state exceptionCode exceptionMessage =
    evaluate (throwSqlException state exceptionCode exceptionMessage)

Let’s see how it worked.

ghci> throwSqlExceptionIO "state" -100 "exception message"
*** Exception: (unknown)
ghci> featureHandleSqlException $ throwSqlException "state" -100 "exception message"
*** Exception: user error (SQL error: SqlError {state = "state", exceptionCode = -100, exceptionMessage = "exception message"})
ghci> featureHandleSqlException $ fail "non-Sql exception"
*** Exception: user error (non-Sql exception)

Summary

In this chapter, you learned

  • how errors can be used in Haskell.

  • the differences in using Maybe and Either.

  • Exceptions in Haskell.

  • the way lazy evaluation handles exceptions.

  • the exceptions for input and output.

  • the way the handle and the throw functions work.

  • about dynamic exceptions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.4.181