Alex Rivera | Logout

How do laziness and exceptions work together in Haskell?

Asked 2012-06-22T03:42:32.633
9

The question is similar to this question. However, this one is about exceptions, not about lazy I/O.

Here is a test:

{-# LANGUAGE ScopedTypeVariables #-}

import Prelude hiding ( catch )
import Control.Exception

fooLazy :: Int -> IO Int
fooLazy m = return $ 1 `div` m

fooStrict :: Int -> IO Int
fooStrict m = return $! 1 `div` m

test :: (Int -> IO Int) -> IO ()
test f = print =<< f 0 `catch` \(_ :: SomeException) -> return 42

testLazy :: Int -> IO Int
testLazy m = (return $ 1 `div` m) `catch` \(_ :: SomeException) -> return 42

testStrict :: Int -> IO Int
testStrict m = (return $! 1 `div` m) `catch` \(_ :: SomeException) -> return 42

So I wrote two functions fooLazy which is lazy and fooStrict which is strict, also there is two tests testLazy and testStrict, then I try to catch division by zero:

> test fooLazy
*** Exception: divide by zero
> test fooStrict
42
> testLazy 0
*** Exception: divide by zero
> testStrict 0
42

and it fails in lazy cases.

The first thing that comes to mind is to write a version of the catch function that force the evaluation on its first argument:

{-# LANGUAGE ScopedTypeVariables #-}

import Prelude hiding ( catch )
import Control.DeepSeq
import Control.Exception
import System.IO.Unsafe

fooLazy :: Int -> IO Int
fooLazy m = return $ 1 `div` m

fooStrict :: Int -> IO Int
fooStrict m = return $! 1 `div` m

instance NFData a => NFData (IO a) where
  rnf = rnf . unsafePerformIO

catchStrict :: (Exception e, NFData a) => IO a -> (e -> IO a) -> IO a
catchStrict = catch . force

test :: (Int -> IO Int) -> IO ()
test f = print =<< f 0 `catchStrict` \(_ :: SomeException) -> return 42

testLazy :: In
Edit
Report

1 Answer

9

Firstly (I'm not sure if you know this already), the reason the catch doesn't work with the lazy case is the

1 `div` 0

expression isn't evaluated until it is needed, which is inside the print function. However, the catch method is applied just to the f 0 expression, not the whole print =<< f 0 expression, so the exception isn't caught. If you did:

test f = (print =<< f 0) `catch` \(_ :: SomeException) -> print 42

instead, it works correctly in both cases.

If you want to make a catch statement though that forces complete evaluation of the IO result, instead of making a new instance of NFData, you could write a forceM method, and use that in the catchStrict method:

forceM :: (Monad m, NFData a) => m a -> m a
forceM m = m >>= (return $!) . force

catchStrict :: (Exception e, NFData a) => IO a -> (e -> IO a) -> IO a
catchStrict expr = (forceM expr `catch`)

(I'm a bit surprised that forceM isn't inside the Control.DeepSeq library)


Regarding your comment:

No, the rule is the exception is only thrown when the value is computed, and that is only done when it is needed by haskell. And if haskell can delay the evaluation of something it will.

An example test function that doesn't use $!, but still causes an exception straight away (so the normal catch will catch the divide by zero exception) is:

fooEvaluated :: Int -> IO Int
fooEvaluated m = case 3 `div` m of
  3 -> return 3
  0 -> return 0
  _ -> return 1

Haskell is forced to evaluated the "3 `div` m" expression, as it needs to match the result against 3 and 0.

As a last example, the following doesn't throw any exception, and when used with the test function returns 1:

fooN
answered 2012-06-22T04:24:09.867

Your Answer