Search code examples
javalambdajava-8functional-interface

Why casting is required with compose method but not with andThen Method


I have following expression that gets executed successfully:

Function<Long,Long> y = ((Function<Long,Long>)(x -> x*x)).andThen(x -> x+1).andThen(x -> x+2);

I understand why casting is required with the first lambda expression here. But following lambda gives error that "x+1" is not a valid operation for the second compose lambda expression

Function<Long,Long> y = ((Function<Long,Long>)(x -> x*x)).compose(x -> x+1).compose(x -> x+2);

I was able to resolve the above error using casting with compose:

Function<Long,Long> y = ((Function<Long,Long>)(x -> x*x)).compose((Function<Long,Long>)x -> x+1).compose(x -> x+2);

I have following questions:

  1. Why do we need casting with compose calls but not with andThen calls?
  2. Why do we need casting with intermediate compose calls but not with terminal compose calls?

Solution

  • Why do we need casting with compose calls but not with andThen calls?

    The two methods are different. compose() takes a function whose input is of a type that is not necessarily the same as the current function's parameter type. Here's a slightly modified example to show that the compiler did not have to assume Long:

    Function<Long, Long> f = (x -> x * x);
    Function<String, Long> g = f.compose(Long::parseLong);
    

    You can observe that f.compose() has a type argument of type String. In the above code, it's inferred from the assignment context (i.e., the compiler knows the input is String-typed because the resulting function is being assigned to a Function<String, Long> variable).

    When it comes to .andThen(), however, things are simpler for the compiler : the type parameter <V> is for the output of the given function (not for the input, as is the case for compose). And because it already knows the input type, it has all the information: .andThen(x -> x+1) can only have Long as output type, because Long + int will produce long, boxed to Long. The end.

    Why do we need casting with intermediate compose calls but not with terminal compose calls?

    Now, think about it, what happens if I wrote this?

    Function<String, Long> g = f.compose(Long::parseLong).compose(Long::parseLong);
    

    What happens is that the compiler is ready to infer the <V> of the last .compose() to String because of the assignment context (see above).
    Question is: Should it assume String for the intermediate .compose()? The answer is Yes in this case* (because Long.parseLong only takes a string, there's no overload), but the compiler doesn't do that; it's a known limitation.

    I can get it to work with f.<String>compose(Long::parseLong).compose(Long::parseLong); (which of course breaks my last .compose() call for obvious reasons, but you get the idea.

    In other words, you can fix it with

    A type witness

    ...<Long>compose(x -> x + 1).compose(x -> x + 2)
    

    An explicit parameter type (my preferred option)

        ...compose((Long x) -> x + 1).compose(x -> x + 2)
    

    *I say "yes in this case" because you cannot expect the compiler to always know the type. It's unambiguous here because Long.parseLong with a single parameter is not overloaded, so we can argue that the compiler could infer the intermediate .compose()'s <V> as <String>. But that should not be understood to mean that the compiler should be able to perform such inference in all situations. The function passed to .compose() could be one taking any other parameter type. The end to the discussion for now is that the compiler does not support this kind of inference.