Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is there difference between throw and throwIO?

I am trying to get a firm grasp of exceptions, so that I can improve my conditional loop implementation. To this end, I am staging various experiments, throwing stuff and seeing what gets caught.

This one surprises me to no end:

% cat X.hs
module Main where

import Control.Exception
import Control.Applicative

main = do
    throw (userError "I am an IO error.") <|> print "Odd error ignored."
% ghc X.hs && ./X
...
X: user error (I am an IO error.)
% cat Y.hs
module Main where

import Control.Exception
import Control.Applicative

main = do
    throwIO (userError "I am an IO error.") <|> print "Odd error ignored."
% ghc Y.hs && ./Y
...
"Odd error ignored."

I thought that the Alternative should ignore exactly IO errors. (Not sure where I got this idea from, but I certainly could not offer a non-IO exception that would be ignored in an Alternative chain.) So I figured I can hand craft and deliver an IO error. Turns out, whether it gets ignored depends on the packaging as much as the contents: if I throw an IO error, it is somehow not anymore an IO error.

I am completely lost. Why does it work this way? Is it intended? The definitions lead deep into the GHC internal modules; while I can more or less understand the meaning of disparate fragments of code by themselves, I am having a hard time seeing the whole picture.

Should one even use this Alternative instance if it is so difficult to predict? Would it not be better if it silenced any synchronous exception, not just some small subset of exceptions that are defined in a specific way and thrown in a specific way?

like image 964
Ignat Insarov Avatar asked Aug 07 '19 19:08

Ignat Insarov


1 Answers

throw is a generalization of undefined and error, it's meant to throw an exception in pure code. When the value of the exception does not matter (which is most of the time), it is denoted by the symbol ⟘ for an "undefined value".

throwIO is an IO action which throws an exception, but is not itself an undefined value.

The documentation of throwIO thus illustrates the difference:

throw e   `seq` x  ===> throw e
throwIO e `seq` x  ===> x

The catch is that (<|>) is defined as mplusIO which uses catchException which is a strict variant of catch. That strictness is summarized as follows:

⟘ <|> x = ⟘

hence you get an exception (and x is never run) in the throw variant.

Note that, without strictness, an "undefined action" (i.e., throw ... :: IO a) actually behaves like an action that throws from the point of view of catch:

catch (throw   (userError "oops")) (\(e :: SomeException) -> putStrLn "caught")  -- caught
catch (throwIO (userError "oops")) (\(e :: SomeException) -> putStrLn "caught")  -- caught
catch (pure    (error     "oops")) (\(e :: SomeException) -> putStrLn "caught")  -- not caught
like image 181
Li-yao Xia Avatar answered Sep 21 '22 05:09

Li-yao Xia