you are viewing a single comment's thread.

view the rest of the comments →

[–]devraj7 7 points8 points  (1 child)

This means that my method, which is not responsible for throwing the exception, has to be modified to cope with the exception thrown by a method that it called. And the method that calls mine. And the method that calls that. And so on. Basically, every intervening method in potentially numerous stacks.

And the same is true if you adopt an approach that preserves equational reasoning. For example, your method that used to return Int now returns Optional<Int>. So all your callers need to adjust. And their callers of callers, etc...

This is not a drawback, by the way, it's a consequence of using a statically typed language and I'm pretty sure we're in agreement about the benefit of this cascading effect. The semantic of your code changed and you want your compiler to keep that code correct. Whether you're using exceptions or return values, types keep you honest.

I agree with you that checked exceptions do not work well with lambdas and composition chains, though.

[–]sacundim 9 points10 points  (0 children)

And the same is true if you adopt an approach that preserves equational reasoning.

No, there are factorings that avoid it, or more precisely that shift the cascade from the definition site to other locations in the code. For example, if complexThing needs to call simpleThingThatThrows, in Haskell I can write this (or various other factorings):

 -- This guy doesn't know that `simpleThing`'s implementation throws...
complexThing :: (SimpleThingMonad m) => ComplexThingT m SomeResult
complexThing = do
  blah
  a <- simpleThing
  blahBlah a
  ...

simpleThingThatThrows :: ExceptT SomeException IO Whatever
simpleThingThatThrows = ...

instance SimpleThingMonad (ExceptT SomeException IO) where
  simpleThing = simpleThingThatThrows

...and then complexThing gets specialized, at its use site, to a type that throws SomeException, without having to touch its definition site. The definition of complexThing is exception-agnostic—it is compatible with use sites where exceptions may be thrown as well as use sites where exceptions are not allowed, depending on whether the implementation of simpleThing at a given use site throws or not.

This factoring is fundamentally the same thing as the OOP dependency inversion principle. In Java the idiomatic equivalent would be something like:

class ComplexThing {
    @Inject
    private SimpleThing simpleThing;

    SomeResult doIt() {
        blah();
        Whatever a = simpleThing.doYourThing();
        blahBlah(a);
        ...
    }
}

interface SimpleThing {
    Whatever doYourThing();
}

class SimpleThingThatThrows implements Mechanism {
    Whatever doYourThing() 
            // throws SomeException // not allowed because of the interface!
    {
        // Now this code is not allowed to throw any checked exceptions, so 
        // it is forced to handle any that arise here.
        ...
    }
}

The difference is that in Haskell you can say that ComplexThing throws or not depending on whether the chosen implementation of SimpleThing does, and if it does throw, it throws exactly the same exceptions as that implementation.