Java Type Signatures
by Mithrandir
This year, I had to do several assignments in Java. Some of them didn’t require me to use a specific IDE but there was one arguing that we should use Eclipse because it helps us in development better than any other tool.
And it does so, with several exceptions. My main disappointment came from the way it handles function signatures. You have the @Override annotation which may help you spot mistakes in overwriting functions from base classes. It helps you doing this but it also fails at some point.
Consider that you have the following interface
public interface ITest { public int function1(int x); public int function2(int x); public int function4(int x); public int function5(int x) throws CustomException; }
with the following CustomException definition.
public class CustomException extends Exception { String msg; public CustomException(String msg){ this.msg = msg; } @Override public String toString(){ return msg; } }
From this, you would understand that the only exception which could be raised is CustomException from function5. Is this really true?
Let’s see an abstrac class using this interface
public abstract class ATest1 implements ITest { @Override public int function1(int x) { if (x == 0) throw new InvalidParameterException("X must not be 0"); return x; } @Override public abstract int function2(int x); @Override public int function4(int x) { return x; } @Override public int function5(int x) throws CustomException { if (x == 0) throw new CustomException("X must not be 0"); return -x; } }
Now, we have one more exception which can be thrown but Eclipse didn’t suggest to add a throws declaration to that method.
Now, consider the following two classes, one of them defining the single left out method
public class Test2 extends ATest1 { @Override public int function2(int x) { return x + 10; } }
while the other overwrites a few other methods and doesn’t throw any exception in them.
public class Test1 extends ATest1 { @Override public int function2(int x) { return x + 1; } @Override public int function1(int x){ return x + 1; } @Override public int function5(int x){ return x + 100; } }
Suppose now, that the user has access only to the interface, he knows only the signatures of functions. In this case, what will the following program do?
public static void main(String args[]){ ITest t1 = new Test1(); ITest t2 = new Test2(); Test2 t3 = new Test2(); Test1 t4 = new Test1(); System.out.println(t1.function1(0)); System.out.println(t1.function2(0)); System.out.println(t1.function4(0)); try{ System.out.println(t1.function5(0)); } catch (CustomException e) { e.printStackTrace(); } System.out.println(t2.function1(0)); System.out.println(t2.function2(0)); System.out.println(t2.function4(0)); try { System.out.println(t2.function5(0)); } catch (CustomException e) { e.printStackTrace(); } System.out.println(t3.function1(0)); System.out.println(t3.function2(0)); System.out.println(t3.function4(0)); try { System.out.println(t3.function5(0)); } catch (CustomException e) { e.printStackTrace(); } System.out.println(t4.function1(0)); System.out.println(t4.function2(0)); System.out.println(t4.function4(0)); System.out.println(t4.function5(0)); }
Each try-catch block above was suggested by Eclipse. As you see in function5 calls, this is not a symmetrical way, giving rise to some errors while refactoring. Hopefully, Eclipse is there and suggests you that there are some exceptions.
Yet, should you make main throw exceptions instead of reporting them, there would be no way to reason why that exception appeared there after changing t4 with t3 in the last line.
However, the above main doesn’t throw CustomException. It throws InvalidParameterException from t2.function1(0), something which is inexplicable from the function’s signature.
This is similar to getting a NullPointerException somewhere where you expected only valid references. Or a segfault in a library function which didn’t present itself as being able to segfault. Something which you cannot fix and the documentation doesn’t explain how it was produced. A bug and a very nasty one.
You may say that this is an unlikely situation, that since you knew what exceptions are thrown, you can add the throws annotation to the interface and then you can document it. However, there are cases of big projects with multiple inheritance paths and lengthy development history with multiple teams of developers. In these instances, it is very common to throw some exception in a method from a class deep down in the hierarchy without needing to report it upwards and document it.
In Haskell, it is simpler. When you change a function’s type to include the possibility of failure, the Maybe you might add must be put in every signature for each function calling you function, you’ll have to change them all. Thus, you’ll surely be forced to document the possibility of failure.
Only head: empty list and Prelude: undefined or exceptions regarding missing patterns can show up (maybe several others but I guess that they can be sorted into one of these three). The last two cases indicate a serious error in the library design and can be fixed, someway or the other. And the first case is the reason why a stack-trace printing debugger for Haskell is so much desired.
But this was a rant about Java, not about Haskell.
EDIT: I hit the post button to early and it is possible that a version with TODOs has appeared in your feed, I’m sorry for that.
I didn’t understand from the article what’s your complaint about Java/Eclipse. The fact that Java allows unchecked exceptions, or that Eclipse didn’t complain about `System.out.println(t4.function5(0));` ?
And I don’t see a better situation in the Haskell land either. To me it’s one of the hardest things to grasp in Haskell. The language has no built-in method for generating/treating errors, but it’s so powerful that you can create libraries for such a thing and that led to a mess in my opinion. Someone counted 8 ways to deal with errors/exception in Haskell, and that was in 2007.
http://www.randomhacks.net/articles/2007/03/10/haskell-8-ways-to-report-errors
Maybe, Either, Error, Exception, or even Failure…
http://www.haskell.org/haskellwiki/Failure
I like Haskell, but this error handling stuff should be fixed. I’d like if there were two ways to throw/catch errors. One for pure code, and another for impure code.
Both and the fact that those annotation are not always true.
In Haskell though, I like the fact that there are many ways to report errors. Maybe I don’t want to go a long road into Error and maybe there’s no need for a fault reason message. I can use Maybe in this case and I’m happy. On the other hand, if I have many things which may fail, I’d go to more advanced error reporting facilities.
To me, those ways seem to be building blocks, just like monads are. Or higher-order functions or simple functions as a matter of fact. You simply choose what’s best for you now and link those together.
You can’t really complain about checked exceptions in Java, as Haskell allows them too, like exception thrown in the IO monad.
Regarding the other point. Well, I see your point that it’s nice to have some building blocks that you can combine how you see fit. The problem arises when you’re mixing libraries. One uses Maybe, another Either, and so on. Sometimes it’s ok because they’re instances of Monad and Applicative, so you can get some consistent syntax. Sometimes though, it won’t work that better.
My feeling at least.
If you want your exception to be visible in the type system, use checked exceptions. Unchecked exceptions are for failures which it is not reasonable to expect a program to recover from, IMHO.
Why do you expect System.out.println(t4.function5(0)); to generate a complaint? The compiler knows that the Test1 subclass throws fewer exceptions — if you changed the type of t4 to ITest your exception would come back.
I find Intellij IDEA better than Eclipse, and they have a free version now, although it doesn’t cover all the web development stuff that the paid version does.
I think we can agree that Haskell is far nicer than Java!