Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type checking with generic Suppliers and lambdas

I have two generic methods, which are designed to force the caller to provide parameters that match type wise:

private <T> void compareValues(Supplier<T> supplier, T value) {
    System.out.println(supplier.get() == value);
}

private <T> void setValue(Consumer<T> consumer, T value) {
    consumer.accept(value);
}

However, when calling them, the compiler reasons differently on what is allowed to pass as parameters:

compareValues(this::getString, "Foo"); // Valid, as expected
compareValues(this::getInt, "Foo");    // Valid, but compiler should raise error
compareValues(this::getString, 1);     // Valid, but compiler should raise error

setValue(this::setString, "Foo");      // Valid, as expected
setValue(this::setInt, "Foo");         // Type mismatch, as expected
setValue(this::setString, 1);          // Type mismatch, as expected


private String getString() {
    return  "Foo";
}

private int getInt() {
    return 1;
}

private void setString(String string) {
}

private void setInt(int integer) {
}

How come? Is the compiler just too clumsy to properly reason about types here, or is this a feature of the type system? If so, what are the rules that lead to this behavior? Also, how would I create a "type safe" version of compareValues without adding artificial parameters, if at all possible?

Please note, that the provided methods merely contain a dummy implementation and do not reflect the code in my actual code base. The focus here are solely the method calls.

like image 531
Patrick Peer Avatar asked May 03 '18 09:05

Patrick Peer


2 Answers

Others have mentioned why this is happening, so here's a solution to get around the problem.

If you create a generic class, separating the passing of the supplier from the passing of the argument, you do not give the compiler the opportunity to choose an intersection type:

public class Comparer<T>
{
    private final Supplier<T> supplier;

    Comparer(final Supplier<T> supplier)
    {
        this.supplier = supplier;
    }

    void compare(T value)
    {
        System.out.println(supplier.get() == value);
    }
}

new Comparer<>(this::getString).compare("Foo"); // Valid, as expected
new Comparer<>(this::getInt).compare("Foo"); // Invalid, compiler error
new Comparer<>(this::getString).compare(1);  // Invalid, compiler error

By separating out this behaviour, you also allow Comparer to do potentially useful things like caching the result of Supplier.get().

like image 179
Michael Avatar answered Oct 20 '22 00:10

Michael


You can tell that the compiler choose an intersection type, by using

javac -XDverboseResolution=deferred-inference

output in one of the cases is:

 instantiated signature: (Supplier<INT#1>,INT#1)void
 target-type: <none>

 where T is a type-variable:
 T extends Object declared in method <T>compareValues(Supplier<T>,T)

 where INT#1,INT#2 are intersection types:
 INT#1 extends Object,Serializable,Comparable<? extends INT#2>
 INT#2 extends Object,Serializable,Comparable<?>
like image 32
Eugene Avatar answered Oct 20 '22 00:10

Eugene