K Man
K Man

Reputation: 628

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

Upvotes: 7

Views: 878

Answers (4)

ajz
ajz

Reputation: 255

Without getting into things too deeply, the bottom line is that the lambda str -> str.length() <= 5 is not necessarily a Predicate<String> as Eugene explained.

That said, negate() is a member function of Predicate<T> and the compiler isn't able to use the call to negate() to help with inferring the type that the lambda should be interpreted as. Even if it tried there might be issues because if multiple possible classes had negate() functions it wouldn't know which to choose.

Imagine that you're the compiler.

  • You see str -> str.length() <= 5. You know that it is lambda that takes a String and returns a boolean but don't know specifically which type it represents.
  • You next see the member reference .negate() but because you don't know the type of the expression to the left of the "." you need to report an error since you can't use the call to negate to help infer the type.

Had negate been implemented as a static function then things would work. For example:

public class PredicateUtils {
    public static <T> Predicate<T> negate(Predicate<T> p) {
        return p.negate();
}

would enable you to write

names.removeIf(PredicateUtils.negate(str -> str.length() <= 5)); //compiles without cast

Because the choice of negate function is unambiguous the compiler knows that it needs to interpret str -> str.length() <= 5 as a Predicate<T> and can coerce the type properly.

I hope that this helps.

Upvotes: 1

WJS
WJS

Reputation: 40047

In the following example

      names.removeIf(str -> str.length() <= 5); //compiles without cast

the expression returns true. If in the following example without the cast, true knows nothing about the method negate()

On the other hand,

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

The expression is cast to Predicate<String> to tell the compiler where to find the method negate. Then it's the method negate() that actually does the evaulation by the following:

   (s)->!test(s) where s is the string argument

Note that you could achieve the same result without the cast as follows:

    names.removeIf(str->!str.length <= 5) 
      // or
    name.removeIf(str->str.length > 5)

Upvotes: 1

Eugene
Eugene

Reputation: 120998

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.

Upvotes: 4

ernest_k
ernest_k

Reputation: 45339

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.

Upvotes: 8

Related Questions