Home > database >  How to catch all exceptions instantiating specific class?
How to catch all exceptions instantiating specific class?

Time:10-31

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 hierarchy

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!"    
  • Related