I am trying to achieve an expressiveness in catching exceptions similar to Java/C#, where I can specify an interface of exceptions I want to catch, otherwise I need to enumerate all possible types.
interface I {void f();}
class AE extends Exception implements I {}
class BE extends Exception implements I {}
try {
throw (new Random().next() % 2 == 0
? new AE()
: new BE());
} catch (I e) {
e.f();
}
class I e where f :: e -> IO ()
data AE = AE deriving (Show)
data BE = BE deriving (Show)
instance Exception AE
instance Exception BE
instance I AE where f _ = putStrLn "f AE"
instance I BE where f _ = putStrLn "f BE"
run m = try @(forall e . (I e, Exception e) => e) m >>= \case
Left er -> f er
Right () -> pure ()
Compiler complains:
GHC doesn't yet support impredicative polymorphism
Original error is produced by ghc 8.10.7.
GHC 9.2.1 has been released. With ImpredicitveTypes turned on the error is different:
• No instance for (Exception (forall e. I e => e))
arising from a use of ‘try’
CodePudding user response:
I'm undeleting this answer again, but please read
Just like in good ol' OOP! :-)
In order to do that, MyException
would be another existential, just like SomeException
itself:
data MyException where
MyException :: forall e. Exception e => MyException
instance Exception MyException
Then you make sure that, during the "wrapping" process that happens when you throw an exception, both AE
and BE
get wrapped in MyException
before getting wrapped in SomeException
:
instance Exception AE where
toException e = SomeException $ MyException e
-- Same implementation for BE
And similarly, when the "unwrapping" happens on catching, make sure both AE
and BE
can unwrap themselves by first making sure it's a MyException
that is wrapped inside SomeException
, and then that it's AE
or BE
respectively that is wrapped inside MyException
:
instance Exception AE where
fromException x = do
MyException m <- cast x
cast m
-- Same implementation for BE
Of course, doing it this way is a bit fragile: both AE
and BE
have to know not only their own "ancestor" type MyException
, but also its ancestor type SomeException
. So in practice both wrapping and unwrapping of MyException
is usually delegated to MyException
's own Exception
instance:
instance Exception AE where
toException e = toException $ MyException e
fromException x = do
MyException m <- fromException x
cast m
And voila: now you can catch either AE
or BE
individually or MyException
, which would apply to them both. Why? Because if you're catching MyException
, as SomeException
gets unwrapped, the MyException
's implementation of fromException
will be used for unwrapping. But if you're catching AE
, then it's AE
's implementation of fromException
that will be used, unwrapping MyException
first and then AE
itself.
Q: Ok, great, now I get all that, but still: can I query for the exception implementing a certain type class?
Yes and no.
If you just have an arbitrary value lying around, you cannot tell if it implements a certain type class, let's call it I
. Because Haskell doesn't have runtime type information, unless you specifically asked for it, and even then, Typeable
can only answer "is this value of type X?"
But it's even more than that: "does this value implement class I
?" is not even a valid question, because it depends on scope. In modules where the I
implementation module is imported, you do have such instance. In modules without such import, the instance doesn't exist. And you could even have two different instances of I
for the same type, in different modules, which are imported in different parts of the program. Sure, it's a bad practice, don't do it, but it can happen.
The root of the problem here is that, unlike OOP interfaces, a class instance is not a property of the type itself, but a relationship between a type (or several types) and a class.
But! You still kinda can.
What you can do is require that any exception that "inherits" (see the diagram above) from MyException
must have an instance of a certain class:
data MyException where
MyException :: (Exception e, I e) => e -> MyException
Now anybody who wants to wrap a value inside MyException
must provide an instance I e
, and that instance will be wrapped inside the MyException
value, and you'll get access to it when you unwrap it. For example:
class I a where
i :: a -> String
instance I AE where
i _ = "This is AE"
throw AE `catch` \(MyException e) -> purStrLn (i e)
I can use the method i
in the handler, because I just unwrapped MyException
and got an I
dictionary out of it.
But the important point is: the dictionary didn't come out of thin air. It was put inside the MyException
value by whoever threw the exception in the first place.
A tangent: you can actually hijack (sort of) the exception wrapping/unwrapping mechanism to allow catching types that haven't been thrown. All you do is make a "dishonest" implementation of fromException
:
data Hijacked = Hijacked deriving Show
instance Exception Hijacked where
fromException x = do
AE <- fromException x -- See if it's an AE
pure Hijacked
throw AE `catch` \Hijacked -> putStrLn "I caught a Hijacked!"