0

I'm trying to make a safe version of mapM that will exclude an element from the result if it throws an exception when executed.

safeMapM :: (a -> IO b) -> [a] -> IO [b]
safeMapM f []       = return []
safeMapM f (x : xs) = do
    restResult <- safeMapM f xs
    appliedResult <- onException (f x >>= evaluate . Just) (return Nothing)
    case appliedResult of
        Just x' -> return $ x' : restResult
        Nothing -> return restResult

It fails to catch anything though. In a simple test case:

safeMapM (\n -> return $ if n == 3 then error $ show n else n) [1,2,3,4,5]

it fails with:

*** Exception: 3
[1,2,"ghci>" 

Why isn't it being caught? Does evaluate not evaluate it fully enough for the error to be caught? Is there a way around this without requiring that the signature be changed to safeMapM :: (NFData a, NFData b) => (a -> IO b) -> [a] -> IO [b], and using deepseq?

Carcigenicate
  • 43,494
  • 9
  • 68
  • 117
  • `evaluate` only evaluates to WHNF. In this case, that's the `Just` wrapper that you add just prior to calling `evaluate`. Remove the `Just` and modify your function accordingly, and it should catch this error. It still won't catch everything without using `deepseq` though. – John L Oct 15 '14 at 21:21
  • This is the reason why I avoid `error` and other asynchronous exceptions: they are much more difficult to catch. – Gabriella Gonzalez Oct 15 '14 at 21:56

1 Answers1

1

evaluate x only evaluates x far enough to get it into WHNF, and Just x is already in WHNF even with x a completely unevaluated thunk. So you have evaluate (Just x), which doesn't involve evaluating x at all!

Instead of binding f x to the function evaluate . Just, you need to use a function that will evaluate x before wrapping it in a Just, something like this:

returnEval :: Monad m => a -> IO (m a)
returnEval x = evaluate x >>= return . return

With that function, you can rewrite your evaluating line like so:

appliedResult <- onException (f x >>= returnEval) (return Nothing)

With that change, the result of your expression goes from [1,2,**exception**] to an immediate exception. Which brings us to the next point: onException is not a "catch" block, it is more like finally: "If something goes wrong, run this code, but in either case give me back the first expression". If you change this to use catch instead, you finally end up with the behavior you wanted to begin with: [1,2,4,5].

module SafeEval where
import Control.Exception

returnEval :: Monad m => a -> IO (m a)
returnEval x = evaluate x >>= return . return

safeMapM :: (a -> IO b) -> [a] -> IO [b]
safeMapM f []       = return []
safeMapM f (x : xs) = do
    restResult <- safeMapM f xs
    appliedResult <- catch (f x >>= returnEval) (\e -> (e :: SomeException) `seq` return Nothing)
    case appliedResult of
        Just x' -> return $ x' : restResult
        Nothing -> return restResult

But as John L says in the comments, this still only catches exceptions at the topmost level: those that are necessary to get into WHNF. If you really want to force eager evaluation of an entire nested expression (you probably don't), you will need a blunter instrument, such as deepseq.

Community
  • 1
  • 1
amalloy
  • 89,153
  • 8
  • 140
  • 205