smf68
smf68

Reputation: 1008

How to specify a java.util.Function that returns another generic type with type covariance?

Suppose I would like to write a method that takes a list and a Function returning another generic type, say Optional. A concrete example for such a method would be one that applies the function to all elements in the list, and returns a list of all elements that didn't result in an empty Optional.

To the best of my understanding, here is how I would write out this method:

public <I, O> List<O> transform(List<I> list, Function<? super I, Optional<? extends O>> function) {
  List<O> result = new ArrayList<>();
  for (I element : list) {
    Optional<? extends O> optional = function.apply(element);
    if (optional.isPresent()) {
      result.add(optional.get());
    }
  }
  return result;
}

The reason I used wildcard arguments for Function are as follows:

Let's test this by first defining two simple types:

public class Animal {}
public class Cat extends Animal {}

Now suppose we're starting with a list of Cats:

List<Cat> catList = new ArrayList<>();

According to the two points I made about transform taking wildcard arguments above, I would like to transform this list into another list of Animals using the following method:

public Optional<Cat> animalToCat(Animal cat) {
  return Optional.empty();
}

This indeed works when I pass a method reference to animalToCat:

List<Animal> animalList = transform(catList, this::animalToCat); // works!

However, what if I don't directly have this method reference available, and would like to store the method in a variable first before passing it to transform later?

Function<Animal, Optional<Cat>> function2 = this::animalToCat;
List<Animal> animalList2 = transform(catList, function2); // does not compile!

Why does the line resulting in animalList compile, but the one resulting in animalList2 doesn't? Through trial and error, I figured out that I can indeed make this compile by changing the type of the variable I'm assigning my method reference to to any of the following:

Function<Animal, Optional<? extends Animal>> function3 = this::animalToCat;
List<Animal> animalList3 = transform(catList, function3); // works

Function<Cat, Optional<? extends Animal>> function4 = this::animalToCat;
List<Animal> animalList4 = transform(catList, function4); // works

Function<? super Cat, Optional<? extends Animal>> function5 = this::animalToCat;
List<Animal> animalList5 = transform(catList, function5); // works

So it seems to be okay to assign the animalToCat method reference, which is clearly one taking an Animal and returning a Optional<Cat>, to other Function types. However, taking the existing animalToCatFunction2 and assigning it to the other types also fails:

animalToCatFunction3 = animalToCatFunction2; // does not compile!
animalToCatFunction4 = animalToCatFunction2; // does not compile!
animalToCatFunction5 = animalToCatFunction2; // does not compile!

I'm very confused as to why it would be okay to treat the this::animalToCat method reference in a way that would make it seem like the returned Optional<Cat> is a covariant type, but that behavior suddenly breaks as soon as the reference is assigned to a variable with a specific type. Is my definition of transform wrong?

Upvotes: 3

Views: 232

Answers (2)

Rok Strniša
Rok Strniša

Reputation: 7212

See STU's reply for the underlying reason.

To get your example to work, you need to help the Java type system a bit by using more type parameters:

<A, B, C extends B, D extends A> List<B> transform(List<D> list, Function<A, Optional<C>> function) {
    List<B> result = new ArrayList<>();
    for (D element : list) {
        Optional<C> optional = function.apply(element);
        if (optional.isPresent()) {
            result.add(optional.get());
        }
    }
    return result;
}

Since the dynamic sub-type of the incoming list can be different to the outgoing one, it's useful to add another sub-type to the example:

class Dog extends Animal {}

Then the example code is:

List<Dog> dogList = new ArrayList<>();
List<Animal> animalList = transform(dogList, this::animalToCat);

Function<Animal, Optional<Cat>> function2 = this::animalToCat;
List<Animal> animalList2 = transform(dogList, function2);

EDIT: Modified the type constraints a bit to make it work with the example in the comment as well:

Optional<Animal> catToAnimal(Cat cat) {
    return Optional.empty();
}

Function<Cat, Optional<Animal>> function2b = this::catToAnimal;
List<Animal> animalList2b = transform(catList, function2b);

Upvotes: 1

STU
STU

Reputation: 121

I believe the reason this does not compile:

Function<Animal, Optional<Cat>> function2 = this::animalToCat;
List<Animal> animalList2 = transform(catList, function2);

whereas this does:

List<Animal> animalList = transform(catList, this::animalToCat);

is that bare method references have a type that is not representable in the user-visible type system.

You can think of this::animalToCat as having the "magic" type Function<contravariant Animal, covariant Optional<Cat>>, but as soon as you convert to the user-defined type Function<Animal, Optional<Cat>> you lose the information about the variance of raw functions themselves.

At that point, you can't assign a Function<Animal, Optional<Cat>> to a Function<Animal, Optional<? extends Animal>> for the same reason you can't assign a Function<Optional<Cat>, Animal>to a Function<Optional<? extends Animal>, Animal>.

C.f. Function1[-T1, +R] in Scala.

Upvotes: 1

Related Questions