Search code examples
javagenericsvavr

Vavr with generics gives incompatible types


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?


Solution

  • 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))
    

    Generic example type inference problem

    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:

    enter image description here

    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:

    enter image description here

    Of course Arrays.asList("a","b","c") returns List<String> and this is the typeshout()` method infers and returns:

    enter image description here

    Specifying explicit type of Some<T>.of() solves the problem as in the Try.of() example:

    enter image description here

    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.