Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does compose() need an explicit cast when andThen() does not?

I'm studying functional composition and have an example:

Function<String, String> test = (s) -> s.concat("foo");
String str = test.andThen(String::toUpperCase).apply("bar"); 

This example compiles and works as expected. However, if I change the order of the composition by using compose(), an explicit cast is required:

String str = test.compose((Function <String, String>) 
                          String::toUpperCase).apply("bar"); 

Without explicitly casting String::toUpperCase to Function <String, String>, there is a compiler error:

Error:
incompatible types: cannot infer type-variable(s) V
    (argument mismatch; invalid method reference
      incompatible types: java.lang.Object cannot be converted to java.util.Locale)
String s = test.compose(String::toUpperCase).apply("bar");
           ^-------------------------------^

The question is why compose() requires explicit cast and andThen() does not in this case?

like image 954
Sharper Avatar asked Oct 14 '21 11:10

Sharper


People also ask

When explicit casting is required in JavaScript?

When you are assigning a larger type to a smaller type, then Explicit Casting is required We all know that when we are assigning larger type to a smaller type, then we need to explicitly type cast it. Lets take the same above code with a slight modification

What happens when you cast a reference to an object?

A cast operation between reference types does not change the run-time type of the underlying object; it only changes the type of the value that is being used as a reference to that object. For more information, see Polymorphism. In some reference type conversions, the compiler cannot determine whether a cast will be valid.

Is casting required when assigning a smaller type to a class?

No Explicit casting required for the above mentioned sequence. We all know that when we are assigning smaller type to a larger type, there is no need for a casting required. Same applies to the class type as well. Lets look into the below code.

What is meant by type casting?

Type casting are of two types they are Automatic type conversion can happen if both type are compatible and target type is larger than source type. No Explicit casting required for the above mentioned sequence.


Video Answer


3 Answers

As the error message shows, the problem is to do with the fact that toUpperCase has an overload which accepts a Locale as an argument, so the compiler error is caused by not knowing which overload String::toUpperCase is supposed to refer to. In principle the compiler should be able to know that a method reference with two arguments (the String object itself is the first argument) can't be a Function, but I suppose there just isn't a rule for this - or rather, there doesn't seem to be a rule for this during inference of generic type parameters, which is why the explicit cast solves the problem.

We can confirm that the overload is indeed what's causing the problem by trying a different method reference like String::trim, which does not have overloads:

// works without an explicit cast
String str = test.compose(String::trim).apply("bar");

So the question now is why the overload doesn't cause a problem when composing with andThen. The difference is that andThen composes the two functions in the opposite order, so the type parameter which needs to be inferred is the output type of String::toUpperCase, which is String regardless of which overload is chosen. So it seems that the compiler has a rule to handle the ambiguity of overloads during type parameter inference when the return type of the method reference will be used for inference, and the overloads have the same return type. Note that a similar rule can't exist for overloads with the same parameter types, because overloads cannot have the same parameter types.

like image 161
kaya3 Avatar answered Oct 24 '22 10:10

kaya3


Let's break this down as an attempt (a rough one) to explore why the compiler issues the error.

1. The difference between andThen and compose in this context

Here are the signatures of the two methods (omitted default):

<V> Function<T, V> andThen(Function<? super R, ? extends V> after)
<V> Function<V, R> compose(Function<? super V, ? extends T> before)

Remembering that Function is declared with two type variables, <T, R>, the most significant difference between after and before in this context is that after is expected to take in a parameter whose data type is R, the current function's return type, whereas the data type of before's parameter is compose's local type variable, V, which the compiler must infer.

Why is this relevant?

2. Why does test.andThen(String::toUpperCase) work?

Among the many things that the compiler is trying to do, it's trying to validate that String::toUpperCase is a valid argument to andThen and compose. To do that, it needs to establish the type of String::toUpperCase, meaning it needs to infer the data type of the method reference in that invocation context. Well, the compiler knows that String::toUpperCase must be a Function, so it just needs to infer the type arguments for Function, which is where the difference above applies:

  • In the case of after (the parameter of andThen), the first parameter of Function<? super R, ? extends V>, is known to be (roughly) R, which has to be the same as the current function's (test's) return type. And the second type parameter, V, which also corresponds to andThen's local type variable, is inferred to whatever the method reference will be declared to return.
  • In the case of before (the parameter of compose), the first parameter's type is unknown, and has to be inferred in context. It corresponds to compose's type variable V. This is where the first difference is.

In Eclipse, the compiler error was The type String does not define toUpperCase(Object) that is applicable here, which gives the clue that the error is related to matching the argument type for String#toUpperCase.
Now, if you see above:

  • after is a Function that takes a String parameter, which makes it somehow simple for the compiler to resolve the String method that String::toUpperCase is targeting. This is a massive topic on its own, but it boils down to the compiler choosing to link the function roughly as the lambda expression (String s) -> s.toUpperCase(). Method references can resolve to instance methods like that, where the first parameter of the function becomes the target of the method invocation (without parameters in the case of Function<T, U>).
  • before could have been resolved in the same way, except that the compiler does NOT know for sure what the type of Function's first type argument is. And because String#toUpperCase is overloaded, the compiler can't just decide to infer it as (String s) -> s.toUpperCase(), because V could have been meant to be of a data type that would force it to resolve the method reference to the overload, String.toUpperCase(Locale). In other words, the compiler is dealing with a chicken-and-the-egg situation (where it needs to know the parameter type V to decide which String#toUpperCase method to link, but can't use String#toUpperCase signature to infer V, because there are two possibilities making it ambiguous).

3. How you solved it

You got this code to compile by casting String::toUpperCase to Function<String, String>. What did that do? It simply got the compiler out of the above dilemma: you told the compiler that V is String, not Locale or any other possibility, which caused the compiler to resolve the method reference as (String s) -> s.toUpperCase() (leaving no room for String.toUpperCase(Locale) to be a valid option)

4. Other ways to solve it

You can effectively do the same thing by forcing an explicit type argument for before's V:

  • test.compose((String s) -> s.toUpperCase()).apply("bar"); will compile because (String s) explicitly types the data type of before's parameter
  • test.<String>compose(String::toUpperCase).apply("bar"); does the same thing by helping the compiler's inference logic, telling it that V is a String, which avoids the chicken-and-the-egg situation referred to above.

This type of solution is not some strange approach, you're essentially doing the same thing that the cast in System.out.println((String)null) does when System.out.println(null) is rejected by the compiler, although these Function methods include generics as a spice.

like image 3
ernest_k Avatar answered Oct 24 '22 10:10

ernest_k


Because of its definition. The method Function#compose is defined as this:

<V> Function<V, R> compose(Function<? super V, ? extends T> before)

... where V is the extra type of input to the before function, and to the composed function. Therefore it additionally requires defining a generic parameter type, because the Function<T, R> knows only T and R. No explicit casting is needed:

String str = test.<String>compose(String::toUpperCase).apply("bar");

Interference doesn't work here because of the ambiguity of String::toUpperCase. More self-explanatory example is a function chain of LocalDateTime -> LocalDate -> String:

Function<LocalDate, String> test = LocalDate::toString;
String str = test.compose(LocalDateTime::toLocalDate).apply(LocalDateTime.now());
like image 1
Nikolas Charalambidis Avatar answered Oct 24 '22 11:10

Nikolas Charalambidis