Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why Doesn't Java 8 Type Inference Consider Exceptions Thrown by Lambdas in Overload Selection?

I have a question regarding Java 8 inference with respect to lambdas and their related exception signatures.

If I define some method foo:

public static <T> void foo(Supplier<T> supplier) {
    //some logic
    ...
}

then I get the nice and concise semantic of being able to write foo(() -> getTheT()); in most cases for a given T. However, in this example, if my getTheT operation declares that it throws Exception, my foo method which takes a Supplier no longer compiles: the Supplier method signature for get doesn't throw exceptions.

It seems like a decent way to get around this would be to overload foo to accept either option, with the overloaded definition being:

public static <T> void foo(ThrowingSupplier<T> supplier) {
   //same logic as other one
   ...
}

where ThrowingSupplier is defined as

public interface ThrowingSupplier<T> {
   public T get() throws Exception;
}

In this way, we have one Supplier type which throws exceptions and one which doesn't. The desired syntax would be something like this:

foo(() -> operationWhichDoesntThrow()); //Doesn't throw, handled by Supplier
foo(() -> operationWhichThrows()); //Does throw, handled by ThrowingSupplier

However, this causes issues due to the lambda type being ambiguous (presumably unable to resolve between Supplier and ThrowingSupplier). Doing an explicit cast a la foo((ThrowingSupplier)(() -> operationWhichThrows())); would work, but it gets rid of most of the conciseness of the desired syntax.

I guess the underlying question is: if the Java compiler is able to resolve the fact that one of my lambdas is incompatible due to it throwing an exception in the Supplier-only case, why isn't it able to use that same information to derive the type of the lambda in the secondary, type-inference case?

Any information or resources which anyone could point me to would likewise be much appreciated, as I'm just not too sure where to look for more information on the matter.

Thanks!

like image 606
paul Avatar asked Sep 01 '15 03:09

paul


3 Answers

If it makes you feel any better, this topic was indeed carefully considered during the JSR-335 design process.

The question is not "why isn't it able", but "why did we choose not to." When we find multiple potentially applicable overloads, we certainly could have chosen to speculatively attribute the lambda body under each set of signatures, and prune those candidates for which the lambda body failed to type-check.

However, we concluded that doing so would likely do more harm than good; it means, for example, that small changes to the method body, under this rule, could cause some method overload selection decisions to silently change without the user intending to do so. In the end, we concluded that using the presence of errors in the method body to discard a potentially applicable candidate would cause more confusion than benefit, especially given that there is a simple and safe workaround -- provide a target-type. We felt that reliability and predictability here outweighed optimal concision.

like image 85
Brian Goetz Avatar answered Nov 03 '22 18:11

Brian Goetz


First of all, you don' have to overload :D - overloading is never a necessity; use 2 different method names, e.g. foo and fooX

Secondly, I don't see why you need 2 methods here. If you want to handle checked and unchecked exceptions differently, it can be done at runtime. To achieve "exception transparency", you can do

interface SupplierX<T, X extends Throwable>
{
    T get() throws X;
}

<T, X extends Throwable> void foo(Supplier<T, X> supplier)  throws X { .. }


foo( ()->"" );  // throws RuntimeException

foo( ()->{ throw new IOException(); } );  // X=IOException

Finally, disambiguity can be achieved throw lambda return type; the compiler uses the return type as if using an argument type for choosing the most specific method. This gives us the idea to wrap the value together with the exception type, as Result<T,X>, a "monad" as they say.

interface Result<T, X extends Throwable>
{
    T get() throws X;
}

// error type embedded in return type, not in `throws` clause

static Result<String,        Exception> m1(){ return ()->{ throw new Exception();};  }
static Result<String, RuntimeException> m2(){ return ()->{ return "str";         };  }

  // better to have some factory method, e.g. return Result.success("str");

public static void main(String[] args)
{
    foo(()->m1());  // foo#2 is not applicable
    foo(()->m2());  // both applicable; foo#2 is more specific
}



interface S1<T> { T get(); }  

static <T> void foo(S1<Result<T, ? extends        Exception>> s)
{
    System.out.println("s1");}
}


interface S2<T> { T get(); }  // can't have two foo(S1) due to erasure

static <T> void foo(S2<Result<T, ? extends RuntimeException>> s)
{
    System.out.println("s2");
}
like image 43
ZhongYu Avatar answered Nov 03 '22 19:11

ZhongYu


Any lambda which could be accepted as a Supplier<T> can also be accepted as a ThrowingSupplier<T>. The following compiles:

public static interface ThrowingSupplier<T>{
    public T get() throws Exception;
}

public static <T> void foo(ThrowingSupplier<T> supplier) {

}

public static String getAString(){
    return "Hello";
}

public static String getAnotherString() throws Exception{
    return "World";
}

public static void main(String[] args) {
    foo(()->getAString());
    foo(()->getAnotherString());
} 

Given the above, you probably don't need this, but if foo must accept a non-throwing Supplier<T>, you can always wrap the Exception-throwing method in a method which launders it into an unchecked Exception:

public static <T> void foo(Supplier<T> supplier) {

}

public static String getAString(){
    return "Hello";
}

public static String getAnotherString() throws Exception{
    return "World";
}

public static String getAnotherStringUnchecked(){
    try{
        return getAnotherString();
    } catch(Exception e){
        throw new RuntimeException("Error getting another string",e);
    }
}   

public static void main(String[] args) throws Exception{
    foo(()->getAString());
    foo(()->getAnotherStringUnchecked());
}
like image 2
augray Avatar answered Nov 03 '22 18:11

augray