sumitsu
sumitsu

Reputation: 1511

Returning a lambda function with parameter types constrained by generic type parameters

Suppose that I want to write a method buildBiFxnWithSameTypeArgs, which produces a two-argument BiFunction whose input arguments are of the same type. It is not known ahead of time (i.e. when buildBiFxnWithSameTypeArgs is invoked) specifically what type that is -- I just want the compiler to enforce that whenever the function returned by buildBiFxnWithSameTypeArgs is invoked, the types of the arguments provided to it must match. The end effect should be, essentially, the lambda equivalent of using the same generic type parameter for the type of two arguments in a method definition.

My initial attempt looks like the following:

public interface ConstrainedBiFunction<I, O> extends BiFunction<I, I, O> {}

public static ConstrainedBiFunction<?, String> buildBiFxnWithSameTypeArgs() {
    return (inputOne, inputTwo) -> String.valueOf(inputOne) + String.valueOf(inputTwo);
}

public void test() {
    buildBiFxnWithSameTypeArgs().apply(Integer.valueOf(1), Integer.valueOf(2));
}

It appears, however, that the wildcard type parameter ? cannot be used in this way; compilation fails on the apply step with the following error:

apply (capture<?>, capture<?> in BiFunction cannot be applied to (java.lang.Integer, java.lang.Integer)

Is it possible to return a lambda function whose parameter types are inferred, then constrained relative to one another in this way?

EDIT: Apologies for the not-very-good question; I had a more complex problem of which I was trying to capture the essence in order to ask the simplest-possible question here, but it looks like I simplified the problem too much. I'm aware, of course, that all classes inherit from Object, so in this over-simplified rendition of the problem, the solutions proposed by @shmosel and @JB Nizet work. I'll post a new question once I've done a better job distilling the original problem.

Upvotes: 0

Views: 1127

Answers (2)

Holger
Holger

Reputation: 298143

While shmosel’s answer correctly points out that using Object as input type should be sufficient for all APIs following the PECS rule, it is possible to fix your original attempt.

Instead of a wildcard, you have to add a type parameter which allows the caller to choose a type:

public interface ConstrainedBiFunction<I, O> extends BiFunction<I, I, O> {}

public static <T> ConstrainedBiFunction<T, String> buildBiFxnWithSameTypeArgs() {
    return (inputOne, inputTwo) -> String.valueOf(inputOne) + String.valueOf(inputTwo);
}

public void test() {
    ConstrainedBiFunction<Integer, String> f1 = buildBiFxnWithSameTypeArgs();
    String s1 = f1.apply(1, 1);
    ConstrainedBiFunction<String, String>  f2 = buildBiFxnWithSameTypeArgs();
    String s2 = f2.apply("hello ", "world");
}

Note that when using it as buildBiFxnWithSameTypeArgs().apply(1, 1);, the compiler will infer Object for T anyway. This allow to pass Integer arguments, but, of course, also anything else, which makes the idea of having the same type for both arguments moot, i.e. buildBiFxnWithSameTypeArgs().apply(1.5, "foo"); works as well, as both arguments are assignable to the same type Object.


There are some real life examples for such constructs dealing with problems not solvable via PECS.

E.g. there is Function.identity() returning an arbitrary typed function having the same input and output type. When passing this function to the toMap collector, you can get a map having the same key or value type as the stream elements.

A more complex example is Comparator.naturalOrder(). Here, the type parameter has a bound that ensures that the elements to compare are indeed Comparable. Since the types don’t implement Comparable<?>, but rather a type referring to itself, the naturalOrder() wouldn’t work without the type parameter.

So you can’t do Comparator.naturalOrder().compare(42, "foo"). But you can do

Comparator<Integer> c1 = Comparator.naturalOrder();
c1.compare(10, 42);
Comparator<String> c2  = Comparator.naturalOrder();
c2.compare("foo", "bar");

Upvotes: 0

shmosel
shmosel

Reputation: 50716

The wildcard means "of a specific, but unknown type". You can't pass any argument that will satisfy a parameter of type ?, since it may be of the wrong type. Change the return type to ConstrainedBiFunction<Object, String> and it will be able to accept any input type, since every class implicitly extends Object:

public static ConstrainedBiFunction<Object, String> buildBiFxnWithSameTypeArgs() {
    return (inputOne, inputTwo) -> String.valueOf(inputOne) + String.valueOf(inputTwo);
}

Note that this can still be used for methods with restrictions on the input type, using the PECS principle. For example:

// input parameter must be of type Integer or any supertype,
// so that we can safely pass in an Integer
String combine(ConstrainedBiFunction<? super Integer, String> function) {
    return function.apply(1, 2);
}

void test() {
    ConstrainedBiFunction<Object, String> concat = buildBiFxnWithSameTypeArgs();
    ConstrainedBiFunction<Integer, String> sum = (a, b) -> String.valueOf(a + b);
    System.out.println(combine(concat)); // "12"
    System.out.println(combine(sum));    // "3"
}

Upvotes: 1

Related Questions