Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 211
Opal Avatar asked Oct 26 '18 12:10

Opal


2 Answers

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.

like image 155
Szymon Stepniak Avatar answered Oct 20 '22 00:10

Szymon Stepniak


TL;DR

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.

  1. Try<List<Integer>> is a subtype of Try<? extends Seq<? extends Number>>.
  2. But 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)

like image 5
Daniel Dietrich Avatar answered Oct 20 '22 02:10

Daniel Dietrich