In haskell, evaluation is lazy, sometimes it's also reffered as "call by need". Historically, there were a lot of lazy languages and haskell was created to unify all of them. Nowaday, it's the only remaining language with lazy evaluation by default, at least to my knowledge.
It has some interesting properties, as it often makes functions more composable and reusable. For example, if you already have at hand a sort
function, you don't need to modify it to only return the top 3 elements: take 3 (sort myCollection)
works and doesn't sort the whole list.
But it also has some pitfals. Lazy IO is often frown upon, and in this post I'll quickly show why.
Exceptions
In haskell, there are a few way to throw exceptions. The main one is from Control.Exception: throwIO :: Exception e => e -> IO a
. To handle exception, there are a couple of functions, the basic one is try :: Exception e => IO a -> IO (Either e a)
. Notice the IO
at the end. That means the only way to catch exceptions is to be in IO
.
Lazy evaluation
Let's examine the following program:
{-# LANGUAGE ScopedTypeVariables #-}
module Main where
import qualified Control.Exception as Exc
main :: IO ()
main = do
result <- Exc.try $ pure (5 `div` 0)
case result of
Left (err :: Exc.SomeException) -> putStrLn $ "Caught error: " <> show err
Right x -> putStrLn $ "Everything went well: " <> show x
If you run this, you'll get the following message <program name>: divide by zero
and the exit code is 1.
So clearly, the try
didn't do its job. SomeException
is the base case of exception, this should catch any exception thrown.
The problem is with lazy evaluation. The problematic operation div 5 0
isn't evaluated until it's actually needed, that is, until it is pattern matched against. And at that point, it's already out of the block covered by try
.
Forcing evaluation
In this simple example, we want to evaluate the expression sooner. There is the evaluate function which does that (only to weak head normal form, details are out of scope for this post). The fix is:
result <- Exc.try $ Exc.evaluate (5 `div` 0)
and this time, the program output Caught error: divide by zero
and returns 0.
Throwing exception in pure code
There is another way to manually throw exception: throw :: Exception e => e -> a
. This one doesn't require to be in IO
. Because of lazy evaluation, exactly when this exception will be thrown is not obvious at all, and thus, it's near impossible to catch. Avoid throw
, instead return an Either
.
Conclusion
Lazyness has subtle interplay with IO and exception. Performing IO action lazily makes handling exception much harder when it's possible at all. The topic of exceptions in haskell is complex and subtle and this is only the tip of the iceberg. For more information, the unliftIO package has some good reading. To perform IO in a streaming fashion, prefer a dedicated library like conduit instead of lazy IO.