-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Null-annotating a lambda parameter type: when can it be, when must it be, what does it mean if it's not? [working decision: applicable when type is present, else inferred] #261
Comments
This can be really hard for us (esp. when fluent APIs, lambdas, and target typing are involved). So far for lambdas we've adopted the convention that:
Although 2 adds a bit more verbosity, it also allows devs to "tell" the analysis what the full augmented type is, when the analysis' inference fails to figure out on its own. It also makes makes lambda params behave more like method params rather than locals. |
Thanks! I think your first convention is a given, since one cannot annotate anyway when the parameter type is omitted (even, I just discovered, if it has It sounds like your tools' current answer is either option 1 or option 3 above. Thinking more about the difference, I realized this: Suppose a tool notices a hard discrepancy in how an explicit lambda parameter type is null-annotated compared to what the target type requires. (This will usually be If that rationale checks out, then the tool is probably free to decide whether to issue a warning about the discrepancy at all, which again I think JSpecify doesn't care about. |
I guess there's a mixed mode too e.g. When it comes to 1 vs 3, we probably fall under option 1. interface NullEater { void eat(@Nullable Object obj); }
interface Foo<T extends @Nullable> {
void takeEater(NullEater e);
void takeConsumer(Consumer<T> c);
}
//
void bar(Foo<@Nullable String> foo) {
foo.takeEater(Object s -> ...);
// ^-- BAD; missing @Nullable
foo.takeConsumer(String s -> ...);
// ^-- BAD; missing @Nullable
} We also take the declared type of lambda param as the source of truth (as augmented type) and check that the lambda body is consistent wrt this declared type. However, in some cases the augmented target type of a lambda may be hard to infer for us, e.g. <A, B> List<B> mapObj(List<A> ls, Function<A, B> mapper) {...}
mapObj(Arrays.asList("Hello", null), x -> ...) The user can opt into declaring the type of the lambda
Apologies if I'm reiterating something obvious with these examples, but I guess my main point is:
|
We don't have any significant implementation experience with lambdas and generic types yet. But I am convinced by @artempyanykh's comments that we (NullAway) would want JSpecify to at least not prohibit the approach that NullSafe is currently taking with respect to nullability annotations and explicitly-typed lambda params (i.e., allow them to be written, and allow for treating them as ground truth). |
Here's what I'm thinking:
Here's some longer rumination I'd typed out (sorry) as I struggled to understand this issue from the bottom up. In Java, every expression has a deterministic compile-time type, and a lambda expression is no exception. This type in turn makes the lambda’s return type and parameter types deterministic as well. Javac takes care of all this for base types. And as long as it can still figure out a type, it lets the lambda omit as many parameter types as it wants to (individually with A nullness analyzer further needs to decide an augmented type for the lambda expression (and thereby for its parameters and return type). Its constraints are:
This determination should not take into consideration any nullness information coming from the body of the lambda. This suggests that type inference should be done in as “friendly” a manner toward the lambda body as possible without breaking any of the three constraints. So, in-types (to the lambda) would “prefer” to be non-null and out-types would prefer nullable. This way, a nullness error is likely to be pinpointed to a specific place in the lambda, where the code depended on (in-type being non-null / out-type being nullable) when that type legitimately can’t be. (Otherwise, I'd expect a more generic (no pun) error about the entire lambda expression type not fitting what its context expects.) |
Note that type-use annotations can't be used with |
Sure about that? This does compile in Java 18:
|
Amusingly, this page cites it as the motivating reason for |
Serves me right for commenting without testing it. Thanks! But, but, but... that's not how I would think that the nullness would be inferred in the absence of an annotation, as with lambda parameters without And I would be inclined to say that annotations there are unrecognized. But maybe I'll look silly when someone tries this out in real code. I was hoping to say that "unrecognized is the conservative option," since we can always define it later. But if the absence of an annotation means that the type is inferred, then obviously we can't safely change it to mean Nullable later :( |
They give the example of The idea that we would do anything other than inference in the unannotated case feels at least conceptually at odds with:
(Maybe only "conceptually" at odds with, since we might not have the same constraints as them. But definitely conceptually :)) |
Then they show After the quickest of skims through the JLS, my doubts are growing: I'm not actually sure that type-use annotations are "supposed" to be usable on var lambda parameters. I'll try to follow up on that. [update: posted to compiler-dev] (Digression: This whole discussion is making me realize that inferred types end up in the bytecode, not just for var local variables but also for var (and otherwise implicitly typed) lambda parameters and other synthetic members, like bridge methods. Those won't have proper nullness annotations. I'll file an issue about that.... [update: #301]) |
So my proposal included this
as a way of not going into it... then all the comments since then have been apparently only about that IF. I'll file it separately, but in the meantime, please note that I'm still awaiting feedback on the proposed resolution. @artempyanykh @msridhar |
If the "not" case, I would assume that the type would be inferred, rather than nullable. Otherwise, a lambda like Your longer rumination suggested that you were at one point viewing inference as part of the picture. Is there something specific that drove you away from that? |
Whoops. I did declare it on-topic to this issue. Sorry! Well, now I'm declaring it belongs to the new #303. :-) [EDIT: FIXED THAT]
Can you explain what appeared to change in a little more detail? Might have been accidental. I'm speaking at the very edges of my comprehension. |
Anyway my real purpose in that comment had been just to give a reminder that the main proposal still needs input. |
I was responding to:
I see the "nullable" there as in conflict with "It's inferred." Since it sounds like we agree on "It's inferred," I think I'm just reading your bullet differently than you intended it. |
As the proposed resolution allows for writing explicit annotations alongside explicit lambda parameter types, it SGTM. I am also fine with not allowing nullness annotations on |
I'd imagine unannotated param types should be treated as 'infer me' types. E.g.
We would want the type of
To me, lambda params are closer to method parameters than to locals. Therefore, I don't see a problem with OTOH, we don't support annotated |
Whoops! That text (now edited) meant to say that if the type is being supplied at all, then it works as described, i.e. no inference. But in your example, and in case of Only if Hopefully that's more palatable! The other part of your comment I'll move to the newer bug. |
Working decision: When a lambda parameter type is given (not If there's variation in how that inference could be done, then there could be possible benefit in standardizing how it works a bit, but let's let that shake out by way of the test suites. |
(similar discussion about record patterns: uber/NullAway#840) |
Current decision: Lambda parameters that are Argument for changing: None. Timing: This must be decided before version 1.0 of the jar. Proposal for 1.0: Finalize the current decision. If you agree, please add a thumbs-up emoji (👍) to this comment. If you disagree, please add a thumbs-down emoji (👎) to this comment and briefly explain your disagreement. Please only add a thumbs-down if you feel you can make a strong case why this decision will be materially worse for users or tool providers than an alternative. Results: This proposal received six 👍s and no other votes. It is finalized. I'll edit the title to reflect that. |
Decision: Lambda parameters that are |
The overall topic of implementation code, #114, is getting split out into #251 and #252 and now this. There is some brief discussion of lambda parameters in the original issue.
What is the deal with null-annotating lambda parameters?
Let's ignore nullness-unspecified types for now.
WHEN THE TARGET TYPE HAS NO WILDCARDS
All the lambda parameter base types will match those of the functional interface method signature exactly -- either they are inferred as such, or they themselves triggered that particular target type to be selected (target typing and/or maybe generic type inference too??), or they are spelled out just for clarity but still must match.
So, bringing nullness annotations into the picture: for ALL type components of the lambda parameter types, IF the types are given at all, our main choices are:
WHEN THE TARGET TYPE HAS WILDCARDS
Back to base types (without nullness annotation/analysis):
Javac target-types the base type of the lambda to
Consumer<String>
. You can coerce it toConsumer<Object>
like this, but I see no real reason to:(Note this would've caused a pointless problem if
a.length()
were being called instead ofa.hashCode()
.)Now to add nullness to the mix (note: assume
@NullMarked
context from here on),First note that a functionally-covariant type parameter should always simulate having no upper bound, i.e. always have
@Nullable Object
as its upper bound:Let's assume the method above is left as it was:
(because if it had gotten annotated
<? super @Nullable String>
then for purposes of nullness annotations it should reduce to the same considerations as the no-wildcard case above.)So all four of these parameter types would fit the target type:
Of course, #s 3 and 4 caused themselves a problem for no good reason. Yeah, it could be fixed by changing
a.hashCode()
toObjects.hashCode(a)
, but why bother? The signature ofm
guarantees we'll never actually be passednull
anyway.This suggests that maybe the same options we listed for the no-wildcards case are appropriate here too.
The text was updated successfully, but these errors were encountered: