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?
Say you have
That means that
xshould be an integer, of course.What does that mean? It means that there was supposed to be an
Integer, but instead there’s just a mistake.Now consider
That means
xshould be an I/O-performing program that returns no useful value. Remember,IOvalues are just values. They are values that just happen to represent imperative programs. So now considerThat means that there was supposed to be an I/O-performing program there, but there is instead just a mistake.
xis not a program that throws an error—there is no program. Regardless of whether you’ve used anIOError,xisn’t a validIOprogram. When you try to execute the programYou have to execute
xto see whether it throws an error. But, you can’t executex, because it’s not a program—it’s a mistake. Instead, everything explodes.This differs significantly from
Now
xis a valid program. It is a valid program that always happens to throw an error, but it’s still a valid program that can actually be executed. When you try to executenow,
xis executed, the error produced is discarded, and_whateveris executed in its place. You can also think of there being a difference between computing a program/figuring out what to execute and actually executing it.throwthrows the error while computing the program to execute (it is a "pure exception"), whilethrowIOthrows it during execution (it is an "impure exception"). This also explains their types:throwreturns any type because all types can be "computed", butthrowIOis restricted toIObecause only programs can be executed.This is further complicated by the fact that you can catch the pure exceptions that occur while executing
IOprograms. I believe this is a design compromise. From a theoretical perspective, you shouldn't be able to catch pure exceptions, because their presence should always be taken to indicate programmer error, but that can be rather embarrassing, because then you can only handle external errors, while programmer errors cause everything to blow up. If we were perfect programmers, that would be fine, but we aren't. Therefore, you are allowed to catch pure exceptions.