"Try"-ing It Out First
Earlier this week we explored how to "catch" exceptions using the functions catch
and handle
. Today we'll learn a couple new tools for this task. The first function we'll look at is try
, but in order to really use it, we'll also have to use evaluate
.
Like catch
, we can use try
to turn our exception into a computation that our program can process and react to gracefully. However, instead of taking an exception handler, this function will simply return the exception using an Either
value.
try :: Exception e => IO a -> IO (Either e a)
The computation produces the result type a
, but could throw an exception e
. So we return the type Either e a
. All this must be done in the IO monad, like we saw with catch
.
Let's recall our previous approach to catching exceptions. Since we had a pure function (sum2Pairs
) that could throw the exception, we would use return
in order to move it into the IO
monad to use catch
. We also needed an explicit type signature on our handler function so that our program knows what exceptions it is trying to catch:
main :: IO ()
main = do
catch (return (sum2Pairs [2, 3, 4]) >>= print) handler
catch (return (sum2Pairs [2, 3, 4, 5]) >>= print) handler
where
handler :: ListException -> IO ()
handler e = print e
Let's try to substitute try
in for these expressions. Once again, we'll explicitly annotate the resulting value with the exception type.
main :: IO ()
main = do
result1 <- try (return (sum2Pairs [2, 3, 4])) :: IO (Either ListException (Int, Int))
print result1
result2 <- try (return (sum2Pairs [2, 3, 4, 5])) :: IO (Either ListException (Int, Int))
print result2
However, this doesn't work the way we want! Our program crashes on the exceptional case!
my-program: NotEnoughElements
The reason for this lies in Haskell's laziness. The exceptional computation doesn't actually occur until we "need" the value, which is when the print statement happens. But by delaying the computation, our program loses the try
context. We can try to wrap the print statement into our "try" block, but it makes our program unnecessarily complicated.
Instead, we have a different tool to help us. This is the evaluate
function.
evaluate :: a -> IO a
At first glance, this seems to be the same type as return
! It takes a pure value and wraps it in the IO
monad. However, it will take care of "evaluating" our expression in a strict (non-lazy) manner. So the computation will occur when we need it to, and we can use "try". If we change our above implementation by swapping evaluate
for return
, then it works!
main :: IO ()
main = do
result1 <- try (evaluate (sum2Pairs [2, 3, 4])) :: IO (Either ListException (Int, Int))
print result1
result2 <- try (evaluate (sum2Pairs [2, 3, 4, 5])) :: IO (Either ListException (Int, Int))
print result2
...
Left NotEnoughElements
Right (5, 9)
So now we have a more reliable way of turning our "pure" computations into expressions where we can catch their exceptions. In the next couple articles, we'll focus some more on what we can do with exceptional data types. Until then, make sure to subscribe to our monthly newsletter so you can stay up to date with the latest news and get access to our subscriber resources!