The blog post introduces an important concept of exception in Haskell.
Exception
When things don't go well, we have to manage that. For example, the div function would not work when divided by 0,
so there needs to be a way of handling this. We did this with Maybe before, but we can use Exception as well.
Exception is a typeclass with many instances like the below.
import Control.Exception -- Import
-- Examples
instance Exception SomeAsyncException
instance Exception IOException
instance Exception ArithException
instance Exception TypeError
--- and moreTo throw an exception, you can do the following.
divide = do
x <- getLine
y <- getLine
let x' = read x :: Float
y' = read y :: Float
if y' == 0 then throw DivideByZero else do return $ x' / y'The above is throwing a DivideByZero exception in the Control.Exception module using throw when y provided
by the user is 0. You can also define your own exception and throw it like below.
data MyError = ErrorA deriving Show
instance Exception MyError
throw ErrorA -- => ***Exception: ErrorAYou don't need to define functions for your error to be an instance of Exception, but it needs to belong
to the Show typeclass.
Catching
The exception needs to be caught for it to be displayed to the user. As it involves displaying on the
screen, the exception can only be caught in impure code or the IO monad. Let's catch the exception from
divide in the main IO action.
-- Function for catching an exception in an IO action
catch :: Exception e => IO a -> (e -> IO a) -> IO a
main :: IO Float
main = do
catch divide (\e ->
do
let t = e :: ArithException
putStrLn "You divided by 0. "
return 0
)The above catch function takes an IO action with a potential exception, a handler for the exception, and
returns an IO action of the same type. Here, the function is used on divide with an ArithException handler
as DivideByZero is a value constructor of the ArithException datatype. The handler prints "You divided by 0."
to warn user, while returning 0 to ensure it is an IO Float, the same type as divide.
You could have used SomeException for the exception since all the exceptions are SomException, but it is
not the best practice as the handler should be defined differently for each exception. When catching mulitple
exceptions, we can do the following:
f = <expr> `catch` \ (ex :: ArithException) -> handleArith ex
`catch` \ (ex :: IOException) -> handleIO exThe above looks good, right? No. Actually, it is a bad practice to use catch like that, because it might catch
an IOException from the handleArith handler. What we want is to catch ArithException or IOException only from <expr>.
To accomplish this, we can use the catches function.
-- Catching multiple exceptions
catches :: Exception e => IO a -> [Handler (e -> IO a)] -> IO a
-- Rewritten with catches
f = <expr> `catches` [Handler (\ (ex :: ArithException) -> handleArith ex),
Handler (\ (ex :: IOException) -> handleIO ex)]Try
Another way of "catching" an exception is by using the try function. It takes an IO action
and outputs an IO action of type Either that contains the exception in Left or the result of computation
of in Right.
-- Try function
try :: Exception e => IO a -> IO (Either e a)
main :: IO Float
main = do
result <- try divide :: IO (Either ArithException Float)
case result of
Left ex -> do
putStrLn "You divided by 0. "
return 0
Right val -> return valIt achieves the same thing as catch except that try can get intrupted by an asynchroneous exception while catch can't.
Also, when you want to catch an exception from a pure function, you need to force execution of the pure function by using evaluate.
-- Pure divide function that throws exception
divide x y
| y == 0 = throw DivideByZero
| otherwise = x / y
-- Using evaluate for pure function execution
main = do
result <- try (evaluate(divide 5 0)) :: IO (Either ArithException Float)
case result of
Left ex -> do
putStrLn "You divided by 0. "
Right val -> print valAside from try, catch, and catches, there are other predefined functions like tryJust, catchJust, and finally. If you
are curious about them, google them! (Googling is one of the most important skill for a programmer. )
When to use Exception
We have covered quite a lot about Exception, but how do we choose between Maybe, Either, or Exception?
The simplest answer is: use Maybe and Either as much as possible unless it is unavoidable to use Exception
because Exception is not really functional. It is unavoidable in cases where you are dealing with IO, threads, and system. Otherwise,
just try using Maybe or Either.
Exercises
This is an exercise section where you can test your understanding of the material introduced in the article. I highly recommend solving these questions by yourself after reading the main part of the article. You can click on each question to see its answer.
Resources
- Philipp, Hagenlocher. 2020. Haskell for Imperative Programmers #27 - Exceptions. YouTube.