Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does negate() require an explicit cast to Predicate?

I have a list of names. On line 3, I had to cast the result of the lambda expression to Predicate<String>. The book I'm reading explains that the cast is necessary in order to help the compiler determine what the matching functional interface is.

However, I don't need such a cast on the following line because I don't call negate(). How does this make any difference? I understand that negate() here returns Predicate<String>, but does the preceding lambda expression not do the same?

List<String> names = new ArrayList<>();
//add various names here
names.removeIf(((Predicate<String>) str -> str.length() <= 5).negate()); //cast required
names.removeIf(((str -> str.length() <= 5))); //compiles without cast
like image 680
K Man Avatar asked Feb 22 '20 01:02

K Man


2 Answers

It is not quite just because you call negate(). Take a look at this version, which is very close to yours but does compile:

Predicate<String> predicate = str -> str.length() <= 5;
names.removeIf(predicate.negate());

The difference between this and your version? It's about how lambda expressions get their types (the "target type").

What do you think this does?

(str -> str.length() <= 5).negate()?

Your current answer is that "it calls negate() on the Predicate<String> given by the expression str -> str.length() <= 5". Right? But that's just because this is what you meant it to do. The compiler doesn't know that. Why? Because it could be anything. My own answer to the above question could be "it calls negate on my other functional interface type... (yes, the example will be a little bizarre)

interface SentenceExpression {
    boolean checkGrammar();
    default SentenceExpression negate() {
        return ArtificialIntelligence.contradict(explainSentence());
    };
}

I could use the very same lambda expression names.removeIf((str -> str.length() <= 5).negate()); but meaning str -> str.length() <= 5 to be a SentenceExpression rather than a Predicate<String>.

Explanation: (str -> str.length() <= 5).negate() does not make str -> str.length() <= 5 a Predicate<String>. And this is why I said it could be anything, including my functional interface above.

Back to Java... This is why lambda expressions have the concept of "target type", which defines the mechanics by which a lambda expression is understood by the compiler as of a given functional interface type (i.e., how you help the compiler know that the expression is a Predicate<String> rather than SentenceExpression or anything else it could be). You may find it useful to read through What is meant by lambda target type and target type context in Java? and Java 8: Target typing

One of the contexts in which target types are inferred (if you read the answers on those posts) is the invocation context, where you pass a lambda expression as the argument for a parameter of a functional interface type, and that is what is applicable to names.removeIf(((str -> str.length() <= 5)));: it's just the lambda expression given as argument to a method that takes a Predicate<String>. This does not apply to the statement that is not compiling.

So, in other words...

names.removeIf(str -> str.length() <= 5); uses a lambda expression in a place where the argument type clearly defines what the type of the lambda expression is expected to be (i.e., the target type of str -> str.length() <= 5 is clearly Predicate<String>).

However, (str -> str.length() <= 5).negate() is not a lambda expression, it's just an expression that happens to use a lambda expression. This is to say that str -> str.length() <= 5 in this case is not in the invocation context that determines the lambda expression's target type (as in the case of your last statement). Yes, the compiler knows that removeIf needs a Predicate<String>, and it knows for sure that the entire expression passed to the method has to be a Predicate<String>, but it wouldn't assume that any lambda expression in the argument expression would be a Predicate<String> (even if you treat it as a predicate by calling negate() on it; it could have been anything that is compatible with the lambda expression).

That's why typing your lambda with an explicit cast (or otherwise, as in the first counter-example I gave) is needed.

like image 160
ernest_k Avatar answered Sep 27 '22 17:09

ernest_k


I do not know why this has to be so confusing. This can be explained with 2 reasons, IMO.

  • Lambda expressions are poly expressions.

I will let you figure out what this means and what the JLS words are around it. But in essence these are just like generics:

static class Me<T> {
    T t...
}

what is the type T here? Well, it depends. If you do :

Me<Integer> me = new Me<>(); // it's Integer
Me<String>  m2 = new Me<>(); // it's String

poly expressions are said that they depend on the context of where they are used. Lambda expressions are the same. Let's take the lambda expression in isolation here:

(String str) -> str.length() <= 5

when you look at it, what is this? Well it's a Predicate<String>? But may be A Function<String, Boolean>? Or may be even MyTransformer<String, Boolean>, where:

 interface MyTransformer<String, Boolean> {
     Boolean transform(String in){
         // do something here with "in"
     } 
 } 

The choices are endless.

  • In theory .negate() called directly could be an option.

From 10_000 miles above, you are correct: you are providing that str -> str.length() <= 5 to a removeIf method, that only accepts a Predicate. There are no more removeIf methods, so the compiler should be able to "do the correct thing" when you supply that (str -> str.length() <= 5).negate().

So how come this does not work? Let's start with your comment:

Shouldn't the call to negate() have provided even more context, making the explicit cast even less necessary?

It seems this is where the main problem starts with, this is simply not how javac works. It can't take the entire str -> str.length() <= 5).negate(), tell itself that this is a Predicate<String> (since you are using it as an argument to removeIf) and then decompose further the part without .negate() and see if that is a Predicate<String> also. javac acts in reverse, it needs to know the target in order to be able to tell if it is legal to call negate or not.

Also you need to make a clear distinction between poly expressions and expressions, in general. str -> str.length() <= 5).negate() is an expression, str -> str.length() <= 5 is a poly expression.

There might be languages where things are done differently and where this is possible, javac is simply not that type.

like image 36
Eugene Avatar answered Sep 27 '22 16:09

Eugene