Could anyone please explain why this code:
interface Lol {
default Try<Seq<? extends Number>> lol() {
return Try.of(List::empty);
}
}
class LolImpl implements Lol {
@Override
public Try<Seq<? extends Number>> lol() {
return Try
.of(() -> List.of(1, 2, 3))
//.onFailure(Object::hashCode)
;
}
}
fails to compile if I uncomment onFailure
statement? No idea what happens here. How to improve it?
You can call Try.of()
with explicit generic type returned to satisfy compiler checks. Something like:
Try.<Seq<? extends Number>of(() -> List.of(1,2,3))
Try.of()
returns type Try<T>
where T
is the type returned by the supplier. And because List.of(T t...)
returns List<T>
, then the final type seen by the compiler is Try<List<Integer>
, which is not what the method returned type defined. Java generics with specific type are invariant and they don't support covariant or contravariant substitutions, so List<Integer> != List<Number>
.
Working example:
import io.vavr.collection.List;
import io.vavr.collection.Seq;
import io.vavr.control.Try;
interface Lol {
default Try<Seq<? extends Number>> lol() {
return Try.of(List::empty);
}
}
class LolImpl implements Lol {
@Override
public Try<Seq<? extends Number>> lol() {
return Try
.<Seq<? extends Number>>of(() -> List.of(1, 2, 3))
.onFailure(t -> System.out.println(t.getMessage()));
}
public static void main(String[] args) {
System.out.println(new LolImpl().lol());
}
}
Output:
Success(List(1, 2, 3))
Further investigation shown that this is most probably a generic compiler problem. Take a look at following plain Java example:
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
interface Some<T> {
static <T> Some<T> of(Supplier<T> supplier) {
return new SomeImpl<>(supplier.get());
}
default Some<T> shout() {
System.out.println(this);
return this;
}
class SomeImpl<T> implements Some<T> {
private final T value;
public SomeImpl(T value) {
this.value = value;
}
}
static void main(String[] args) {
final Some<List<CharSequence>> strings = Some.of(() -> Arrays.asList("a", "b", "c"));
}
}
This code compiles without any issue and compiler infers type returned by Arrays.asList()
from the expected type on the left side:
Now, if I call this Some<T>.shout()
method, which does nothing and returns Some<T>
, compiler infers the type not from the expected variable type, but from the last returned type:
Of course Arrays.asList("a","b","c")
returns List<String>
and this is the type
shout()` method infers and returns:
Specifying explicit type of Some<T>.of()
solves the problem as in the Try.of()
example:
I was searching Oracle documentation on type inference and there is this explanation:
The Java compiler takes advantage of target typing to infer the type parameters of a generic method invocation. The target type of an expression is the data type that the Java compiler expects depending on where the expression appears.
Source: https://docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html#target_types
It looks like this "depending on where the expression appears" in this case means inferred type from the previously returned exact type. It would explain why skipping shout()
method makes compiler aware, that we expect Some<List<CharSequence>>
and when we add shout()
method it starts returning Some<List<String>>
, because this is what shout()
method sees from the returned type of Some.of()
method. Hope it helps.
The answer to your question is related to Java's type inference in conjunction with type variance (covariance in our case). It has nothing to do with Vavr in particular.
Try<List<Integer>>
is a subtype of Try<? extends Seq<? extends Number>>
.Try<List<Integer>>
is not a subtype of Try<Seq<? extends Number>>
.Change the return type of the lol()
method(s) to the Try<? extends Seq<? extends Number>>
and all will compile fine.
Let us take a detailed look.
public Try<Seq<? extends Number>> lol() { // line 1
return Try.of(() -> List.of(1, 2, 3)) // line 2
//.onFailure(Object::hashCode) // line 3
;
}
The lol()
method does return a value of type Try<Seq<? extends Number>>
(see line 1).
The return statement in line 2 returns an instance of Try
that is constructed using the factory method Try.of(...)
. In Vavr 0.9.x, it is defined the following way:
static <T> Try<T> of(CheckedFunction0<? extends T> supplier) {
// implementation omitted
}
The compiler infers:
// type T = Seq<? extends Number>
Try.of(() -> List.of(1, 2, 3))
because it needs to match both, the return type of the method lol()
and the CheckedFunction0
signature of the factory method Try.of
.
This compiles fine, because the supplier
function returns a value of type ? extends T
, which is ? extends Seq<? extends Number>
, which is compatible with the actual return type List<Integer>
(see TL;DR section above).
If we now uncomment the .onFailure
part (line 3), then the generic type argument T
of the factory method Try.of
does not have the scope of the return type of lol()
anymore. The compiler infers T
to be List<Integer>
because it always tries to find the most specific type that is applicable.
.onFailure
returns a value of the type List<Integer>
because it returns exactly the same type if its instance. But Try<List<Integer>>
is not a subtype of Try<Seq<? extends Number>>
(see TL;DR section above), so the code does not compile anymore.
Making the lol()
method covariant in its return type will satisfy the compiler:
// before: Try<Seq<? extends Number>>
Try<? extends Seq<? extends Number>> lol() { // line 1
return Try.of(() -> List.of(1, 2, 3)) // line 2
.onFailure(Object::hashCode); // line 3
}
Btw, to define the correct generic variance throughout the type hierarchy of Vavr, especially for the collections, was one of the hard parts when creating Vavr. Java's type system is not perfect, there are still several things we can't express with Java's generics. See also my blog post "Declaration-Site Variance in a Future Java"
Disclaimer: I'm the creator of Vavr (formerly Javaslang)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With