Skip to content

! should partipicate in null shorting #1163

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

Closed
stereotype441 opened this issue Aug 19, 2020 · 66 comments
Closed

! should partipicate in null shorting #1163

stereotype441 opened this issue Aug 19, 2020 · 66 comments
Labels
nnbd NNBD related issues

Comments

@stereotype441
Copy link
Member

This issue captures discusison that has been happening in dart-lang/sdk#43093.
Quoting @lrhn:

@eernstg We have formally specified null shortening now, right?
I would expect ! to be included in the suffix operators that are shortened, but please check.

I definitely do not want

 x?.foo?.bar;

and

 x?.foo!.bar;

to behave differently when x is null or x.foo is non-null. Users should be able to think that ?. can be replaced by !. when they know that the value will not be null.

Quoting @eernstg:

Given that ! is a selector when it is used to check for non-null values, I believe the consistent approach is to treat ! accordingly, by adding a rule along the following lines:

If `e` translates to `F` then `e!` translates to: `PASSTHRU[F, fn[x] => x!]`

I believe this would ensure that x?.foo!.bar evaluates to null when x is null, and throws when x is non-null but its foo is null.

Also, it seems less useful to let x?.foo!.bar mean (x?.foo)!.bar, because this would imply that we didn't mean ?. in the first place.

Proposed spec update here.

@stereotype441 stereotype441 added the nnbd NNBD related issues label Aug 19, 2020
dart-bot pushed a commit to dart-lang/sdk that referenced this issue Aug 19, 2020
Postfix increments and decrements are null-shorting expressions
(e.g. `x?.y++.isEven` only calls `isEven` if `x` was non-null), but it
is still being decided whether postfix `!` should be null shorting
(i.e. whether `x?.y!` should fail if `x` is null).  See
dart-lang/language#1163.

Previously, the analyzer was inconsistent about whether it considered
`!` to participate in null shorting; as a result, when analyzing an
expression like `x?.y!`, the analyzer would fail to call
`FlowAnalysis.nullAwareAccess_end`, resulting in corrupted flow
analysis state, which could lead to a crash.

This change makes the analyzer treat `!` as *not* participating in
null shorting, which is consistent with what is currently written in
the spec and implemented in the CFE.

Fixes #43093.

Change-Id: Ie69c5c29f226fe1a0282d0e7a1e079778dc700c3
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/159147
Commit-Queue: Paul Berry <[email protected]>
Reviewed-by: Konstantin Shcheglov <[email protected]>
@munificent
Copy link
Member

I worry that users will find the resulting type of the expression to be very unintuitive. A couple of examples:

expectInt(int i) {}

test(List<int?>? list) {
  expectInt(list?.first!); // Error.

  list?.first! + 2; // Error.

  var x = list?.first!;
  x.isEven; // Error.
}

So the user is explicitly writing ! after an expression, but the thing they get in response is nullable!

One option would be to only null short ! when immediately followed by another selector:

list?.first! // Equivalent to: `(list?.first)!`
list?.first!.isEven // Equivalent to: `(list != null) ? (list.first!.isEven) : null`

In other words, we short ! when it appears like !. or ![...], but not when a subexpression of other kinds of expressions. I don't know if I like this idea or not. :-/

@leafpetersen
Copy link
Member

What about making it an error to apply ! to a null shortened expression? I think it's always the case that anything you write of the form e! where e is a null shorting expression can be rewritten by replacing the null shorting operators in e with !, right? That is, foo?.bar()?[3]! is equivalent to foo!.bar()![3].

Alternatively, say that it does participate in null shorting, but make it a warning to avoid the confusion that @munificent points out (which I agree is really bogus - it's really surprising that an expression of the form e! can evaluate to null).

@stereotype441
Copy link
Member Author

What about making it an error to apply ! to a null shortened expression? I think it's always the case that anything you write of the form e! where e is a null shorting expression can be rewritten by replacing the null shorting operators in e with !, right? That is, foo?.bar()?[3]! is equivalent to foo!.bar()![3].

Personally I'm not a huge fan of this. I buy Lasse's argument (from our meeting this morning) that allowing ! to participate in null shortening is valuable because it lets you write code like a?.b!.c (meaning a == null ? null : a.b!.c).

Alternatively, say that it does participate in null shorting, but make it a warning to avoid the confusion that @munificent points out (which I agree is really bogus - it's really surprising that an expression of the form e! can evaluate to null).

I like this, assuming that when you say "make it a warning", you mean "make it a warning when a null-shorted e! is not immediately followed by another selector". The warning could link to some documentation where we explain why it doesn't do what the user probably wants, and the analysis server could provide a quick fix that changes a?.b! into (a?.b)! (or a semantically equivalent expression like a!.b or a!.b!, depending on the type of b).

@bwilkerson @pq for visibility on the warning/quick fix idea.

@jakemac53
Copy link
Contributor

jakemac53 commented Aug 20, 2020

expectInt(list?.first!); // Error.

For situations like this - if we don't do null shortening then it really converts it to list!.first!, which doesn't seem right to me. If users want to assert the entire chain is non-null, they should use !. and not ?. throughout.

@leafpetersen
Copy link
Member

leafpetersen commented Aug 20, 2020

Ok, let me try to pull together some pieces here. Suppose we have:

class A {
  int get zero => 0;
  int? get zeroOrNull => null;
}

Suppose we have a local variable A? a = A()

Consider a?.zero!:

  • If ! participates, this has type int? and evaluates the same as a?.zero
    • This seems bad to me, the user never wants this (should be error or warning if we take this interpretation)
  • If ! does not participate this has type int and evaluates the same as (a?.zero)!
    • This seems likely to be what the user wanted when they wrote this.

Consider a?.zeroOrNull!

  • If ! participates, this has type int? and evaluates the same as (a == null) ? null : a.zeroOrNull!
    • The user may have wanted this interpretation: they want to assert that when a is non-null, a.zeroOrNull is non-null
  • If ! does not participate this has type int and evaluates the same as a!.zeroOrNull!
    • The user may have wanted this interpretation. In fact, if they wrote int x = a?.zeroOrNull, the obvious fix for the error is to add a ! to the end, and then be puzzled that the error doesn't go away.

Consider a?.zero!.isEven:

  • If ! participates, then this has type bool?, and the ! is a no-op and should be an error or warning.
  • If ! does not participate, then this has type bool, and the meaning is `(a?.zero)!.isEven
  • This seems likely to be what the user intended when they wrote this.

Consider a?.zeroOrNull!.isEven

  • If ! participates, then this has type bool?, and the meaning is (a == null) ? null : a.zeroOrNull!.isEven.
    • A user might reasonably write this and expect it to work.
  • If ! does not participate, then this has type bool, and the meaning is `a!.zeroOrNull!.isEven
    • It's possible that this is what the user intended when they wrote this, but seems less likely.

Summarizing, if we agree with my evaluations above... let's say that the user falls into the pit of success if they get the interpretation they expected, the pit of non-failure if they get an error, and the pit of failure if they sometimes get the wrong behavior, and the pit of despair if the always get the wrong behavior.

If ! participates, we get the following:

Expression Intention Behavior Which pit
a?.zero! (a?.zero)! Error Non-failure
a?.zeroOrNull! Unknown (a == null) ? null : a.zeroOrNull! Failure
a?.zero!.isEven `(a?.zero)!.isEven Error Non-failure
a?.zeroOrNull!.isEven More likely (a == null) ? null : a.zeroOrNull!.isEven (a == null) ? null : a.zeroOrNull!.isEven Likely success

If ! does not participate, we get the following:

Expression Intention Behavior Which pit
a?.zero! (a?.zero)! (a?.zero)! Success
a?.zeroOrNull! Unknown a!.zeroOrNull! Failure
a?.zero!.isEven `(a?.zero)!.isEven `(a?.zero)!.isEven Success
a?.zeroOrNull!.isEven More likely (a == null) ? null : a.zeroOrNull!.isEven a!.zeroOrNull!.isEven Likely failure

Does that summary seem fair? The likely success/failure example could be argued to be a failure. Other things that I got wrong? Are there other forms we should be considering?

@lrhn
Copy link
Member

lrhn commented Aug 20, 2020

@munificent If a user writes list?.first! and expects a non-nullable result, they get a nullable result and will likely recognize this because nullable results are highly visible. They hopefully then writes (list?.first)!, which is not the same as list!.first when first can be nullable. The (list?.first)! will throw if either list or list.first is null. However, that's not a usage I'd recommend, you should not be using ! to report null errors, you should use it when you know that it won't throw, and even if you do use it just to report null errors, I'd recommend list!.first! instead as more readable.

@leafpetersen The summary seems fine. It's worth noticing that the two Failure entries are both where the intention is unknown. That is, we don't know whether it's a failure or success.

I'll still argue that consistency and simplicity are sufficient to choose ! always participating in the null-shortening. rather than trying to guess at the user intent.

  • All other suffix operations participate.
  • The ! participates in cascades: a?..zero!, a?..zeroOrNull!, a?..zero!.isEven and z?.zeroOrNull!.isEven all include the !, and x?..foo should always be equivalent to x?.foo when there is only one part of the casecade and the value isn't used.
  • It's easy to explain: "! is a suffix operator", "?. can always be replaced by !.".

Also, if ! does not participate, then there is no way to make it. a?.foo! cannot be converted to! applied to foo in any way, it means the same as (a?.foo)!, but if it does participate, you can use parentheses to make it non-participating: a?.foo! and (a?.foo)! means different things, both of which might be the intent of someone.

@eernstg
Copy link
Member

eernstg commented Aug 20, 2020

@leafpetersen wrote:

If ! participates, we get the following: ...
If ! does not participate, we get the following: ...

It looks like there's a rather strong argument in favor of ! not participating, because we have two outcomes of 'Success' over 'Non-failure' for the latter and a single, weaker 'Likely success' over a 'Likely failure' for the former.

However, I agree with @lrhn that it is more consistent to let ! participate than not. I also think it's important that "the other choice" cannot directly be expressed if ! does not participate.

But even more, we can't say that we are avoiding the parentheses: Being a selector, ! won't apply to any other expression than t <selector>* where t is <primary> or <constructorInvocation>, plus a similar situation in a <cascadeSectionTail>. For instance, a + b! won't mean (a + b)!, simply because a selector can't be applied to a + b. So the intuition that we can "just add ! at the end of any expression" won't work, it generally parses as applied to some subexpression near the end of the expression as a whole. This means that the parentheses will be needed very frequently in any case, and it won't help developers much to allow them to omit the parentheses in very special cases like a?.b!.

I believe we can't change ! to be a postfix operator, because that's deeply breaking, and then we couldn't even have a.b!.c().

Next, I'm not convinced that a developer who writes a?.zero! actually intends it to mean (a?.zero)!. That would be natural to assume if it would work to say "just add ! at the end", but we know that this won't work in general. Also, in the case where the intention is actually (a?.zero)!, the natural way to write it would be a!.zero. Do we really want to bend the language design in a direction where "a?.zero! works", if it should have been written differently in the first place?

The same consideration applies to a?.zero!.isEven: If the intention is (a?.zero)!.isEven then the proper way to write it would presumably be a!.zero.isEven.

What could the intention be for a?.zeroOrNull!? I find it natural to assume that ! participates in null-shorting, in which case it is a null check on the zeroOrNull that we will apply when we have already detected that a is non-null. This works, so why would that be a failure? The argument would presumably be that this is useless: The whole expression is still nullable. That is true, but even that could make sense: We may have business logic reasons for insisting that zeroOrNull returns a non-null value when we call it, and we might still be happy about the whole expression evaluating to null in the case where a is null.

With a?.zeroOrNull!.isEven the intention matches what I described, and I don't see why that shouldn't be a plain success. ;-)

So I'd still prefer that ! participates in null-shorting.

However, should we then emit a hint in the case where ! occurs at the end of an expression (that is: as the last selector) and the expression type is potentially nullable? As I mentioned above, I think there are legitimate usages. But it is possible that this is so rare that we'd prefer to hint against it.

@munificent
Copy link
Member

@leafpetersen, thanks for writing up all of the interesting cases. When I look at them, the question I find myself asking is, "If a user wrote that, would it be useful?" By that I mean:

  1. Does it do something meaningful?
  2. Is there no better way to express the same behavior?

The only two cases that I see being useful—where I would be happy to see a user write them—are:

a?.zeroOrNull!
a?.zeroOrNull!.isEven

And they are only useful if ! null-shorts. The second case here is particularly compelling. I think it would be actively frustrating if I could not write that, akin to the pain we feel today when you want to stuff an await in the middle of a method chain and can't.

If we short, the other two cases become pointless but harmless:

a?.zero!
a?.zero!.isEven

Fortunately, they give you a static warning letting you know that what you wrote is pointless.

On the other hand, if we don't short, every one of these cases becomes what I'd consider bad code. There's always a better way to express the same thing:

a?.zero!              // Should instead be: a!.zero
a?.zeroOrNull!        // Should instead be: a!.zeroOrNull!
a?.zero!.isEven       // Should instead be: a!.zero.isEven
a?.zeroOrNull!.isEven // Should instead be: a!.zeroOrNull!.isEven

I see a compelling argument that shorting is the right behavior. I was worried that it would be hard to explain why an expression that textually ends with ! might have a nullable type, but now I could explain how that behavior is better than the alternative.

@leafpetersen
Copy link
Member

So, oddly, I find myself becoming a hold out here.

@munificent

"If a user wrote that, would it be useful?" By that I mean:

I actually think this is the wrong question, or at least, only one of the questions that needs to be asked. The question I find myself asking is "what did the user most likely intend?". And I cannot convince myself that a user who writes a?.b! actually intended to only apply the ! to the read of b and not to the entire expression. We may think of ! as a selector, but users, I claim will not. They will think of !. as a selector, and ! as a postfix operator. I see a very clear pit of failure here where:

  • a user writes: takesInt(a?.b)
  • the analyzer yells at them for passing a nullable thing where a non-nullable thing was expected
  • the user does the completely obvious fix and writes takesInt(a?.b!)
  • the analyzer yells at them for passing a nullable thing where a non-nullable thing was expected
  • the user is sad and concludes that Dart is one weird language

@lrhn

  • It's easy to explain: "! is a suffix operator", "?. can always be replaced by !.".

I strongly doubt this statement.

I'm fairly strongly of the opinion right now that if we do this, we should make it an error to terminate a null-shorting expression with a ! selector, because it almost never is what the user wants, and when it is, there are better ways to write it.

I think that if we allow it to be the case that an expression of the form e! can have nullable type, this will be very, very confusing to users.

@eernstg
Copy link
Member

eernstg commented Aug 21, 2020

Here's a similar story:

  • a user writes: takesInt(a?.b)
  • the analyzer yells at them for passing a nullable thing where a non-nullable thing was expected
  • the user does the completely obvious fix and writes takesInt(a!.b)

Writing a?.<anything> is an explicit request for a nullable result, and it seems reasonable to say that the toolbox for removing nullability should include both "change ?. to !." and "add ! at the end".

We can still hint in the cases like a?.zeroOrNull! where e! has a nullable type, at least in situations where the context accepts null (such that the developer doesn't get any other diagnostics).

@lrhn
Copy link
Member

lrhn commented Aug 21, 2020

Summary: I firmly believe that a?.b!.c must have the ! participating, anything else is strictly less useful and potentially dangerous, and making a?.b! by itself not participate is also useless (and dangerous).
The only thing I think could be potentially useful, is making a?.b! invalid.
I just don't think the extra exception is worth the complexity. I'd rather encourage the analyzer to provide hints whenever a ! is used unnecessarily, where the non-nullability of the expression isn't actually need, and that's not just in that one case.

(Basically: What Bob said!)


  • the user does the completely obvious fix and writes takesInt(a?.b!)
  • the analyzer yells at them for passing a nullable thing where a non-nullable thing was expected

The analyzer also yells at them for using a ! on a non-nullable thing. If it's a good error message, it tells them that b has a type of int.

the user is sad and concludes that Dart is one weird language

I definitely do not want a?.b!.c to not include !.c in the null-shortening. If we do that, then they can truly claim that Dart is a weird language. When you grok that ?. is now shortening, it looks exactly like something which should shorten.
One thing I have repeatedly mentioned as a design goal is that the user to expect that changing e?.select to e!.selector is always allowed if you know e evaluates to a non-null value. Since we shorten e?.foo?.bar, we should shorten e?.foo!.bar too.

Then we can still make not make a trailing ! not participate in the shortening, but that's not actually useful. Doing x?.y! where ! does not participate is almost invariably an error, and if not, it should be written as x!.y! (or even x!.y if y happens to be non-nullable). Allowing it, especially silently is directly harmful. The expression looks like it's non-nullable, but it's internally inconsistent because x?. only makes sense if x can actually be null, otherwise you should write x!..
Then it's better to completely disallow a trailing ! because it's never going to be the right thing.

Spreading !s blindly around your code is not something we want to encourage. Allowing it silently is doing the users a disservice.

If we let a trailing ! participate in the null-shortening, and you write var x = a?.b! you are effectively asserting that b is not null. It's slightly weird because it's technically unnecessary, but it might be considered useful as a run-time sanity check.

If we make a trailing ! on a null-shortening into an error, then a?.b! is invalid. There is no practical use for it in the type system, so it's no big loss. It's an extra special case, though, and it's not very consistent.
Should we disallow a..b! as well, the ! is equally invalid there. (I'd actually say "yes", because I still want cascades and null-shortening to behave similarly).
How about a.b!; where the value is not used (expression statement, any void context)? The ! is also unnecessary there.
Or int? x = a.b!; - the ! has no type effect, it's just asserting that b is not null, even though it's not important. That's exactly the same as a?.b! where ! participates in the null shortening.

If we start disallowing useless things, I think we should be consistent about it, otherwise it's just a weird wart on the language, not a useful feature.

If the user gets confused because adding ! doesn't change the type, they need to understand precedence. We should help them with that, perhaps with better warning or error messages, not by making the code do something which isn't useful anyway.
There are many other expressions where adding a ! does not make the expression non-nullable:

  • a + b!
  • a ? b : c!
  • a?..foo!

We expect users to recognize that because they know how ! binds, and how to parse the structure of an expression.
Making ! act differently between x?.y! and x?.y!.foo is not helping that.

I want users to think of ?. the same way they think of ?.., and they should have the same extent.
You need parentheses in the same situations where you want to limit the scope of the cascade/null-shortening.

  • (a?..foo.bar).toString() // limits cascade to bar
  • (a?.foo.bar).toString() // limits the null-shortening to bar.

@leafpetersen
Copy link
Member

Here's a similar story:

  • a user writes: takesInt(a?.b)
  • the analyzer yells at them for passing a nullable thing where a non-nullable thing was expected
  • the user does the completely obvious fix and writes takesInt(a!.b)

I tested this out on a couple of people last night, neither of them saw this as an option, and neither of them did this.

@leafpetersen
Copy link
Member

  • the user does the completely obvious fix and writes takesInt(a?.b!)
  • the analyzer yells at them for passing a nullable thing where a non-nullable thing was expected

The analyzer also yells at them for using a ! on a non-nullable thing. If it's a good error message, it tells them that b has a type of int.

No it doesn't. b is nullable.

@leafpetersen
Copy link
Member

If the user gets confused because adding ! doesn't change the type, they need to understand precedence.

No, this is not about precedence. Precedence is entirely irrelevant to this discussion.

@leafpetersen
Copy link
Member

There are many other expressions where adding a ! does not make the expression non-nullable:

  • a + b!
  • a ? b : c!
  • a?..foo!

Those are all places where it's very clear that there is an ambiguity going on, and there is a precedence issue here. I expect users to either know about precedence, or use parenthesis, when there is a precedence issue. We are not discussing a precedence issue. We are discussing how the continuation passing transform which gives semantics to null shorting treats !, and that is not something I expect users to understand.

@leafpetersen
Copy link
Member

I want users to think of ?. the same way they think of ?.., and they should have the same extent.

I want many things that I can't have. I think this is an unrealistic desire, and not even a very sensible one. Even if you look at the grammar, it's clear they are difference. A cascade consists of a series of cascade sections. Each section is a collection of selectors (I'm speaking very roughly here). It's reasonably natural to see that ! is part of the cascade section, and if a user gets confused I can explain it to them in almost exactly the words I used earlier in the sentence.

@leafpetersen
Copy link
Member

Here's another way of talking about what's going on. We don't have a ? operator, we only have a ?. operator. We could have chosen to add a !. operator, and a separate ! postfix operator. If we had done so, I think it's fairly clear that the !. operator should participate in null shorting, and the ! operator should not (just as other function applications do not: we don't null short prefix !, why postfix !?).

Given that we have not done so, we're in a bit of a pickle. You can simulate the !. operator using the composition of ! and ., but you have to choose a null shorting behavior, and if you pick the behavior that is right for !. you get the one that is wrong for !, and vice versa. What to do?

Well, one thing you could do is encode the difference into the null shorting rules (do a different thing with a terminating !). This seems a little surprising, but might be fine. You probably want to be sure that e! .foo isn't treated as (e!).foo though, and there may be other tricky spots. This is more or less equivalent to adding !. as a separate operator, I think.

Another thing you could do is just not allow ! to terminate a null short (which is what I've proposed) since there is always an equivalent way to write it that doesn't use a terminating !. This may be a little tricky to specify, but seems feasible. The main objection I've heard is consistency, but I'm not sure I buy that as a good argument here. Consistently doing the wrong thing doesn't seem like a feature.

Are there other things we can do here?

@lrhn
Copy link
Member

lrhn commented Aug 21, 2020

For the record, I do not think a participating final ! is a wrong thing. It's not particularly useful, but it's also no completely useless. You can use it as a non-null assertion, even when it has no effect on the type. If you don't want it, you can use parentheses to make the ! bind differently, but you probably won't because the result isn't actually useful.
And it's consistent in that we do make other postfix operators participate: [e], (arg), and ++ (except that we don't actually allow ++ in a cascade section, but we should!) The ! fits nicely into a selector chain with the rest.

I do think a non-participating final ! is wrong, and something we should make an effort to not support. It's never the right thing, and it cannot be changed to associate differently with parentheses.
If we had separate ! and !. operators, I would still not want to make a ! non-participating. So, it's not fairly clear that ! should not participate. To me, it's fairly clear that it shouldn't.

Disallowing a final ! is an option, and definitely a compromise. I don't particularly care for it, I think a lint would be sufficient, and we could make other lints too where a ! is just as unnecessary. I'm also not greatly opposed to it, it just seem like an unnecessarily complication. It's true that "You can't end a ?. selector sequence with a !." is more actionable than the error message that we'd probably give when the expression stays nullable (unless recognize the pattern e?.selectors! in a non-nullable context and we give an equally good error messages).

I can't think of a fourth thing to do about x?.y!. There aren't that many moving pieces. We either have to give it a semantics or make it an error. If we give it a semantics, anything except letting ! participate or not participate in the shortening seems ... well, I can't even come up with another option. So those are the three options. Within each, we can choose which lints, warnings or errors to provide the user with.

Knowing how far a ?. null shortening stretches is important to users. If possible, I'd make it stretch exactly as long as a cascade section: The chain of selectors (a finite set of syntactic structures that you can easily learn), following, possibly ended by an assignment. Then they only have to learn one thing. (Heck, I'd have used the same grammar for ?. selectors and cascade section selectors).
We've pretty much done that (well, except prefix increment operators, because they're desugared to an assignment, but otherwise), it's actually pretty much the same thing. Or rather, I though it was the same thing because ! would behave the same.

Making a trailing ! an error is still OK within this mindset. It's a single exception, syntactically recognizable at a very visible place of the chain, so users will be able to remember it. I don't think they'll be able to remember if it's allowed in cascade section, they'll probably just not do it in both cases (which is fine, it's also useless there). So I think we should disallow a trailing ! in cascade section selector chains too, if we do it for null-shortening selector chains.

(I use "precedence" about any choice about which syntax binds stronger, not just binary operators, so whether a?.b! has ! binding stronger than ?. - participating - or weaker than ?. - non-participating - is a kind of precedence to me. It might not be the correct use of the word).

@leafpetersen
Copy link
Member

It's not particularly useful, but it's also no completely useless.

I think this gets to where we're talking past each other. :) I'm not claiming it's useless. I'm claiming that it is surprising. Users are being taught (via experience, and via the fact that we're telling them so) that e! is what you do when you have a nullable typed thing that you know is non-null, and you want to assert that fact. Making ! participate breaks that. Is it useful? Sure it could be. Does it do what the user expects? I don't believe so. I think (with some evidence) that users will write m?[0]! expecting it to assert that m?[0] is non-nullable, and I think users will read m?[0]! as producing a non-nullable thing.

(I use "precedence" about any choice about which syntax binds stronger, not just binary operators, so whether a?.b! has ! binding stronger than ?. - participating - or weaker than ?. - non-participating - is a kind of precedence to me. It might not be the correct use of the word).

I'm not even really sure - Dart's grammar is weird here. I don't know how to think of ! binding stronger than ?.: m?.(foo!) isn't a thing. But I have a hard time thinking about Dart's grammar - it's very not expression oriented. So maybe I'm too fixated on things that can be disambiguated via parentheses?

@leafpetersen
Copy link
Member

@tatumizer Are you assuming that List.first returns null if empty? It doesn't. Also, the type of the elements of the list matters.

In any case, that is a syntactically valid expression. Whether it has errors and/or warnings depends somewhat on the type of list and somewhat on the outcome of the current discussion.

@leafpetersen
Copy link
Member

Assuming that list is a List<int>? then:

If ! participates, then your code means that:

  • if list is null then list?.first!?.bitLength evaluates to null, and so the whole thing evaluates to 0
  • Otherwise it returns the bit length of the first element, if any, or throws an exception if empty
  • This code has a static warning because ! is applied to a non-nullable expression
  • Removing the ! still has a static warning because ?. is used on a non-nullable expression
  • What you wanted to write was list?.first.bitLength ?? 0

if ! does not participate, then your code means that:

  • if list is null, then list?.first! throws an exception
  • Otherwise it returns the bit length of the first element, if any, or throws an exception if empty
  • This code has a static warning because the second ?. is applied to a non-nullable receiver.
  • What you wanted to write was list?.first.bitLength ?? 0

Note that what you actually wanted to write, given your description of your intended semantics, is the same in either case, and does not involve !.

@leafpetersen
Copy link
Member

int bitLength  = map?["myValue"]!?.bitLength ?? 0;

(the exclamation mark is to assert that map (if not null) for sure contains the entry "myValue" (if not, I'd like to get an exception).

I think this is a quite legitimate use case - it would be surprising if the compiler rejected it.

There is no proposal that the compiler reject this.

In either semantics, the second ?. is unnecessary, and is a static warning.

If ! participates, the code (with or without the unnecessary ?.) does what you want.
If ! does not participate, the code does not do what you want (with or without the unnecessary ?.).

@lrhn
Copy link
Member

lrhn commented Aug 24, 2020

I'm claiming that it is surprising. Users are being taught (via experience, and via the fact that we're telling them so) that e! is what you do when you have a nullable typed thing that you know is non-null, and you want to assert that fact.

I'm not sure I agree that we're telling them that, at least not that alone.
The ! is an operator and it binds in certain ways, and we want users to understand those ways.
We are also teaching users how to read ?. and ../?.., and how to use !. instead of ?.

I do admit that my fixation on how things bind might be a little too syntax based. The question is not how to parse x?.y!, but how to evaluate it. Where does the null shortening stop. I've thought of it as being syntactic - the entire selector chain. I could also see it as semantics based, having a "propagating/contagious/shortening null" value that replaces the value of x when x is null and it's followed by ?., and then every selector after that will propagate the value until we stop the null shortening and convert it back to a normal null. That can happen before the end of the selector chain.

(Does a cascade terminate null shortening? x?.y..z..w should probably work, so I'd say no. However, cascade is not a selector, so I don't think we allow it. I'd like to, but that'd require moving cascades to an entirely different place in the grammar, and likely introduce ambiguities that we'd need to resolve in prose.)

I still think letting ! participate is the least surprising behavior overall, because it makes the grammar more consistent across different similar constructs, even if it might be locally surprising for people who think you can put a ! after anything without considering the context.
I want x?.y! and x?..y! to behave the same (except for the result of the entire expression). I think that is more important than whether x?.y! is non-nullable.

I can't write var res = x?.y![z] and then rewrite it to var tmp = x?.y!; var res = tmp[z];. It doesn't work, but it's not due to whether ! is participating or not, it's because things below a ?. are just not compositional. They depend on the context, so you can't move them out of that context.
Anything happening below ?. (or ../?..) is special. I want them to be special in the same, predictable, way.

I think that users who understand null-shortening ?. and cascades will very likely begin to think that x?.foo.bar! will null-shorten the ! as well. We can't know, because there are not a lot of those people yet (and I'm probably not representative).
Making it not-null shorten, but only if it's trailing, but not if followed by ?. or ![ or !(, can actually confuse those people.

So, I think the safest move for now is to prohibit trailing ! operators in null shortenings and cascades.
Then we can start figuring out whether there is a user-need for it, in either usage. There may very well not be, because it isn't particularly useful to begin with.

@leafpetersen
Copy link
Member

I'm not sure I agree that we're telling them that, at least not that alone.

From https://dart.dev/null-safety/understanding-null-safety

“Casting away nullability” comes up often enough that we have a new shorthand syntax. A postfix exclamation mark (!) takes the expression on the left and casts it to its underlying non-nullable type. 

This is public documentation written by a member of the language team. A recent internal email to Dart users with a TL;DR about null safety used basically the same description. We can certainly change what we are telling people, but it's definitely what we're telling them.

I want x?.y! and x?..y! to behave the same (except for the result of the entire expression). I think that is more important than whether x?.y! is non-nullable.

I really think this is an odd thing to prioritize. Even putting aside "except for the result of the entire expression", which is doing rather a lot of work here, . and .. are just fundamentally and deeply different, and I don't think any user expects them to be interchangeable. And they specifically behave differently with respect to null shorting: print(<String?>[null]..first?.length..clear()); prints out [], because the ?. does not short the second ...

So, I think the safest move for now is to prohibit trailing ! operators in null shortenings and cascades.

Why cascades?

@lrhn
Copy link
Member

lrhn commented Aug 24, 2020

Cascades for consistency.

I still think users have a benefit from thinking of "downstream from ..' and "downstream from ?." as roughly the same.
If we say that one of them don't work, I don't expect users to be able to remember whether it's x?.y! or x?..y!, or that they are different.

@leafpetersen
Copy link
Member

Cascades for consistency.

I may have misunderstood you, do you mean one these, or something else?

  • ! may not terminate any cascade section
  • ! may not terminate any cascade section in a null short cascade
  • ! may not terminate the last cascade section
  • ! may not terminate the last cascade section in a null short cascade

@lrhn
Copy link
Member

lrhn commented Aug 24, 2020

I think I'd go for:

  • ! may not terminate a null-shortening (may not be the last selector in the selector chain).
  • ! may not terminate any cascade section (may not be the last selector in the selector chain).

So, the first of those four.

@stereotype441
Copy link
Member Author

On a general note, do we have any examples whatsoever where this has come up in practice? I just took a quick look at the core libraries and didn't see anything, and just grep'ed the ported part of Flutter and didn't find any interactions between ?. and !. This makes me wonder whether this is really the most valuable thing we could be spending time on in the remaining weeks.

I'm only aware of one instance in which this arose concretely: when Michal Terepeta was trying to migrate pageloader (see internal bug 162579669). He tried to change clickOption?.clientX to clickOption?.clientX!, and so on with clientY, screenX, and screenY in https://github.com/google/pageloader/blob/80766100da9fe05d99eb92edd69b7ddfa82cc10e/lib/src/html/html_page_loader_element.dart#L308-L315. (In retrospect the correct migration would probably have been clientOption?.clientX ?? 0 instead). Anyway, this resulted in an analyzer crash (which I fixed in dart-lang/sdk@4280e0a). So it's no surprise you didn't find any interactions between ?. and !, since until recently the analyzer couldn't handle it! I guess the fact that the analyzer crash took so long to discover is pretty good evidence that this isn't a terribly critical feature 😃

@lrhn
Copy link
Member

lrhn commented Aug 26, 2020

For the record, .await would not be a getter (it can't be, you can't use the word await as an identifier in an async function), it just looks like one. It will pretty certainly grammatically be a selector, like .foo, .foo=..., .foo(...), ?.foo, ?.foo=..., ?.foo(...), [], []=, ?[], ?[]=, (...), and !.

Another possible postfix operator is the tightly-binding-cast, as <type> (#193 ), which can be used inside a selector chain: foo.bar as <Bar>.baz (the "<...>" around the type makes it possible to find the end of the type, so it is unambiguous to parse).
It will also be a selector, so you can use it in a cascade section f..bar as <Bar>.baz.

Should x?.bar as <Bar>.baz make as <Bar>.baz participate in the null shortening.

Since as <Bar> and ! are really the same thing if the x.bar has type Bar?, I'd like those to do the same thing, so whatever we do for ! should also be what we do for as <T>.

(I say yes, because I want all selectors to participate, then the reach of the ?. is to the end of the selector chain).

@eernstg
Copy link
Member

eernstg commented Aug 26, 2020

Putting aside the cascades and the ! at the end for a minute, does anybody think that the non-terminating ! should not participate in null shorting?

If we do agree that it should participate then we'd at least cut down on this discussion to be about the terminating ! and cascades.

I tend to think that it would be highly confusing if ! were allowed to throw because row is null in expressions like the following:

return hasActiveBudget
    ? row?.insertionOrder.activeBudget.budgetType
    : row?.insertionOrder.totalBudget!.budgetType;

Assuming that the getters insertionOrder etc. are all non-nullable, the ! in the "else" branch does exactly one thing: It throws if row is null. I don't think this is going to help anybody, and I'd actually prefer to let that ! be null shorting, and get an error/warning that totalBudget isn't nullable.

We do seem to have support for making the non-terminating ! null short from @munificent, @lrhn, and myself.

@lrhn
Copy link
Member

lrhn commented Aug 26, 2020

I am currently assuming that non-terminating ! is participating, and I'm only arguing about terminating !s.

I've been talking about how I think cascades, null-shortening and general selector chains should behave similarly. Talking to Leaf yesterday made me able to explain (I hope) why I think it's important. At least I'll try.

I believe that users think of programs (among other ways) by understanding some concepts by mentally desugaring them to simpler concepts, and by knowing some "safe transformations" (which is probably just a word for refactoring).

Cascade to non-cascade

A cascade, e..selectors1..selectors2, can be understood as desugaring to something like

let tmp = e in {tmp.selectors1; tmp.selectors2; return tmp;}

(A fancy code-like way to say: Evaluate e to a value tmp, then execute the selector chain selectors1 on v, then do the same for selectors2, finally the value of the entire cascade is v. If e is primitive, we don't need the temporary variable.)

I expect users to perform something like this desugaring in their head, in order to figure out what a cascade actually does.
I know that's what I do when reading a cascade.

This desugars cascade .. to normal selector . (and ..[..] to normal [..]).

That is, users do not expect cascade section selectors to be a special thing, they are understood exactly like the same non-cascade selectors.

For this reasoning to be correct, the meaning of the selectors must not change when they are moved from inside a cascade to not being inside a cascade.

We even have a lint which makes users change e..selectors; to e.selectors;. That lint assumes that all you have to do is change .. to ., and then it will preserve semantics. We have trained users to expect that.

Null-aware cascade to plain cascade

A null-aware cascade, e?..selectors1..selectors2, can be understood as:

let tmp = e in tmp == null ? null : tmp..selectors1..selectors2

which can then be further desugared to the non-cascade like above, without even needing an extra variable.

For this reasoning to be correct, the meaning of selectors must not change when moved from a null-aware cascade to a non-null-aware cascade.

(A null-aware cascade can also be desugared as to normal null-aware selectors:

let tmp = e in {tmp?.selectors1; tmp?.selectors2; tmp}

Either works, it all depends on whether the user thinks of e?.. as skipping everything in one step, or skipping each cascade section individually. I'm guessing the former will be more common among users.)

In general, the cascade sections should act the same whether it's a ?.. or a .. cascade. There is no reason to make a difference.

Null-aware cascade to null-aware selector

The unnecessary single cascade lint will likely also recommend changing x?..selectors; to x?.selectors; when it gets updated for null safety, so the meaning of selectors must not change when changed from a null-aware cascade (which behaves the same as a non-null-aware cascade) to a null-aware non-cascade selector.

Non-cascade to cascade.

If you write:

something.selectors1;
something.selectors2;

then we recommend combining them into a cascade:

something
    ..selectors1
    ..selectors2;

For that to be correct, the meaning of a selector chain must not change when moved from a non-cascade to a cascade.

(We'll probably also recommend something?.selectors1;something?.selectors2; to be rewritten as something?..selectors1..selectors2;.)

Conditional null-aware to null-checking selector

Users will think of ?. and !. as different tools for related issues.

If you wanted to write e.selectors, but e is nullable, then:

  • If you know, for some reason, that e is going to be non-null, use e!.selectors.
  • If you don't know that, use e?.selectors. You'll have to handle the null later then, after the selectors.

That means that if you are looking at e?.selectors and somehow figure out that a is definitely going to be non-null, it's safe to change it to e!.selectors.

For this reasoning to be correct, it's important that the meaning of selectors does not change when they are moved from a null-aware ?. to a non-null-aware !..

Consequences for x?.y!

It's a given what x.y! does by itself, and x..y! currently follows that behavior.

By the reasoning above, it's important that if x?.y! or x..y!/x?..y! does anything, the selector chain behaves the same as .y!. The final result of the entire expression might differ, but the meaning of the selector chain .y! should not change, otherwise reasoning based on desugaring doesn't hold. (Also, things which look similar should behave similarly).

It's acceptable to make x?.y! invalid. Users won't try to desugar something which isn't accepted, so it doesn't matter that what it would desugar to is valid. They'll just learn that this particular combination is invalid. However, that does break the "Null-aware cascade to null-aware selector" section. It's probably a rare case to go from x?..selectors; to x?.selectors;, but it can happen when removing the next-to-last cascade section from a null-aware cascade.

I do think that if we make x?.y! invalid, we should do the same for x..y!/x?..y!. Not because one desugares to the other (it does, it's just not because of that), but because I want users to use their understanding of how far a cascade section reaches to understand how far a null-shortening reaches.

Also, combining multiple cases (and assuming that cascades do participate in null shortening), then something like:

foo?.bar..baz!; // created by removing ..qux; from the cascade

should be rewritten as foo?.bar.baz!; which would then become invalid.

Making x..y! invalid breaks the "Non-cascade to cascade" section and . If you have x.y!;x.z!; and convert it to x..y!..z!;, it stops being valid. Arguably that's a good thing, because the rewrite has made it obvious that the ! isn't actually needed. We could complain about x.y!; as well, because we can see the value isn't used.

Conclusion

All in all, I will prefer to make ! always participate in null shortening. (And I want cascades to always participate.)

If not, I prefer to make it an error to have a ! as the last selector of the selector chain of a cascade or a null-aware selector (?. or ?[..]). If the chain containing ! is continued by another selector, then it is valid.

I want this because I expect users to use one kind of reasoning for every selector chain. Making some cases invalid is better than having the same selector syntax do different things in different cases.

@lrhn
Copy link
Member

lrhn commented Aug 26, 2020

I just noticed that https://github.com/dart-lang/language/blob/master/accepted/future-releases/nnbd/feature-specification.md says:

The grammar of expressions is extended to allow any expression to be suffixed with a !.

That's not a sufficient syntactic specification. The linked grammar makes it a selector, which makes it clear how it parses. Maybe the phrasing should be changed to say something similar to what we say for ?[...] (that it's added to the grammar of selectors).

@leafpetersen
Copy link
Member

That's not a sufficient syntactic specification.

It's not intended to be. That's why there's a grammar.

@leafpetersen
Copy link
Member

Conditional null-aware to null-checking selector

Users will think of ?. and !. as different tools for related issues.

If you wanted to write e.selectors, but e is nullable, then:

  • If you know, for some reason, that e is going to be non-null, use e!.selectors.
  • If you don't know that, use e?.selectors. You'll have to handle the null later then, after the selectors.

That means that if you are looking at e?.selectors and somehow figure out that a is definitely going to be non-null, it's safe to change it to e!.selectors.

For this reasoning to be correct, it's important that the meaning of selectors does not change when they are moved from a null-aware ?. to a non-null-aware !..

I don't understand what you are trying to say here. Or rather, I don't think you're saying what you mean to say.

Under the assumption that e does not evaluate to null, then:

  • e?.selectors is equivalent to (e)!.selectors in either interpretation
  • e!.selectors is equivalent to (e)!.selectors in either interpretation

I think what you might be trying to say is that e is a path e0.<selector>, and the last element of the path is nullable, but you know it doesn't evaluate to null, but e may evaluate to null via null shorting, in which case it is true that e0.<selector>?.selectors is not equivalent to e0.<selector>!.selectors unless ! participates. Is that what you're trying to say?

@leafpetersen
Copy link
Member

Cascade to non-cascade

I don't have a lot to say about this. I understand broadly where you're coming from, but I don't find it all convincing to drive the design of a major part of the language (e!) off of a particular desired refactoring for a corner case of a relatively infrequently used feature.

Non-cascade to cascade.

If you write:

something.selectors1;
something.selectors2;

then we recommend combining them into a cascade:

something
    ..selectors1
    ..selectors2;

This is a completely invalid refactoring, and I don't believe that we recommend it.... unless something is a trivial expression with no potential side effects, in which case this almost certainly remains valid in either treatment of !.

@leafpetersen
Copy link
Member

My basic view on the situation is as follows.

The situation as it stands

We have two constructs ?. and !.

Currently ! has a completely straightforward and easy to understand semantics: it universally takes the expression that it binds to syntactically to it's left, and checks that it is not null.

Currently, e?.<stuff> has a complicated semantics. It "skips out" of some but not all continuation of the expression to the left of it if e evaluates to null. So for example e?.foo skips foo if e is null, but e?.foo + whatever doesn't skip out of the + whatever, and (e?.foo).bar doesn't skip bar. So lots of rules to remember. Oh, and one more: currently e?.foo! doesn't skip out of the bang.

The proposed change

The proposal is to change this. Under this proposal, ! gets complicated. An expression e! no longer is guaranteed to evaluate to null, and may have nullable type (yes, I realize that not all types have a non-nullable version, I wrote that spec). You need to look deeper into e to understand the semantics of e!.

Simple re-factors like replacing var x = e; var y = x! with var x = e! don't work any more.

On the other hand, ?. gets one fewer exception. Some re-factors around cascades may not work any more.

Relative importance

So we have two opposing desires: for various reasons, there's a desire to make a change that improves the behavior of ?. at the expense of the understandability and predictability of !. How to evaluate? On balance, it's hard for me to weight ?. over ! here. ! is the fundamental operator of nullability. It is not definable, and null safe values cannot be used in without it except by casting them to dynamic. So making it less predictable seems to me to have a very high cognitive cost.

On the other hand, ?., while useful, is a convenience feature, and adding one more or less rule to its list of exceptions doesn't seem to me to add a lot of value.

Definability

Note that if you really want a null shorting bang operator, you can define it already:

extension NullCheck<T> on T? {
  T get notNull => this!;
}

void main() {
  List<int>? l = null;
  print(l?[0].notNull.isEven);  // Prints "null"
}

@leafpetersen
Copy link
Member

For the record, .await would not be a getter

For the record, yes... I know.

@leafpetersen
Copy link
Member

I'm only aware of one instance in which this arose concretely: when Michal Terepeta was trying to migrate pageloader (see internal bug 162579669). He tried to change clickOption?.clientX to clickOption?.clientX!, and so on with clientY, screenX, and screenY in https://github.com/google/pageloader/blob/80766100da9fe05d99eb92edd69b7ddfa82cc10e/lib/src/html/html_page_loader_element.dart#L308-L315. (In retrospect the correct migration would probably have been clientOption?.clientX ?? 0 instead)

Note that if you look at this code, it's quite clear that the expectation in the original migration was that clickOption?.clientX! would produce a value of non-nullable type since that's the expected context of occurrence.

@lrhn
Copy link
Member

lrhn commented Aug 27, 2020

I think what you might be trying to say is that e is a path e0., and the last element of the path is nullable, but you know it doesn't evaluate to null, but e may evaluate to null via null shorting, in which case it is true that e0.?.selectors is not equivalent to e0.!.selectors unless ! participates. Is that what you're trying to say?

Yes.
If you write something...something?.foo and you know that the ?. there is not going to see a null, you can replace that ?. it with !..
I expect users to think that this is a safe refactoring, but that is only true if !. participates whenever ?. does.

@lrhn
Copy link
Member

lrhn commented Aug 27, 2020

Non-cascade to cascade

...

This is a completely invalid refactoring, and I don't believe that we recommend it.... unless something is a trivial expression with no potential side effects, in which case this almost certainly remains valid in either treatment of !.

Yes, we only recommend it when it doesn't change semantics in a way that affects the correctness of the program. It's not in the style guide, but it's a lint, and I've seen plenty of code reviews where someone is asked to change x.foo(); x.bar(); to x..foo()..bar();. We do, effectively, recommend to use cascades when possible.

The rewrite would not be valid if x..foo! is invalid, but x.foo! is valid. (Which is an argument against me wanting trailing !s on cascades to be invalid).

@lrhn
Copy link
Member

lrhn commented Aug 27, 2020

While clickOption?.clientX! was intended to make the entire expression non-null, someone else could also write something?.potentiallyNull! and intend it to only document that potentiallyNull is non-null.

The failure modes of the two are different.

  • In the former case, but where ! participates, the type system immediately tells them that ! did not do what they expected. They might be confused, but they can look at ?. and learn that the things it extends over includes !. They will need to know what it extends over anyway, I don't consider that an exception.
  • In the latter case, but where ! does not participate, their code is accepted silently, but may throw unexpectedly at run-time.
    The something?.potentiallyNull! will occur in a nullable context, and changing the expression to non-nullable is valid, and won't give a warning unless we explicitly introduce an unneeded ! warning.

Being non-participating is more likely to introduce accidental errors.

@lrhn
Copy link
Member

lrhn commented Aug 27, 2020

I tried looking at what other languages do. Not a lot of conclusion from that, though.

  • Kotlin: Has ?. and !!, but ?. is not shortening. (It's even using ?. and !! in the default example of https://play.kotlinlang.org/ - their non-null promotion is quite clever, it recognizes a?.b!!.c as guaranteeing a is non-null).

  • C♯: ?. is shortening. There is a T-typed getter on Nullable<T>, so using that is automatically included in the shortening. Example: https://repl.it/repls/PeruQuintessentialGreenware#main.cs
    There should be a ! operator too, but I can't make it work on general types (it does work on string, and it's not participating).

  • Swift: Their ? types are proper nestable Option classes, and they have promotion on is-checks. They have null-shortening ?. and a ! operator, and ! participates in the shortening (String? s = nil; print(s?.length.magnitude!); is an error because ! cannot be used on a non-nullable type, checked on http://online.swiftplayground.run/).

@munificent
Copy link
Member

I think we might have different definitions of "useful". See my comment.

I agree, we do. I think that's a very non-standard use of the word, and it's one I don't agree with. I think it's probably better that we not use the term in that sense, or we're going to keep talking past each other.

OK, let me try to say this differently. I'm looking at the question of "What choice should we the language designers make here?" I'm trying to decide what behavior I consider it "useful" for us to put into the language. Maybe "worthwhile" is a better term.

So, trying to evaluate which behavior is worthwhile for us to put into the language:

  • If we make ? participate in null-shorting, it gives users a way to express something useful that they cannot easily express otherwise. So, it's worthwhile because it increases the expressiveness of the language.

  • If we make ? not participate in null-shorting, then the resulting behavior is identical to something they can already express. And the way they can already express that behavior is, I think, clearer and better in every respect.

They may have intended it to not short, but I still think there is still always a better way to express the same behavior.... It may take users a little while to learn that capability, but they do get a nice bit of expressiveness in return.

I believe very strongly that this is a bad justification for making design choices. If you say "sure writing this piece of syntax looks like it should do Y, but users already have a way to write Y, so we're going to define it to do X" then you end up with Perl. And I don't want to end up with Perl.

No, I think you're exactly backwards here. Perl's motto literally is "there's more than one way to do it". If we don't treat "you can already do that easily in Dart" as a reason to not add something, then it will encourage us to add more redundant ways to express the same thing, which is how you get Perl. I don't want Perl either. (Though I admit UI-as-code certainly gives you extra redundant ways to make lists...)

Taking your quote, what I propose is extending it like, "sure writing this piece of syntax looks like it should do Y, but users already have better a way to write Y, so we're going to define it to do X which they don't have a way to do yet and will want".

Why give them two ways to express Y (one of which is kind of bad) and no ways to express X?

@leafpetersen
Copy link
Member

Why give them two ways to express Y (one of which is kind of bad) and no ways to express X?

I'm sorry, but I am completely unconvinced by this line of reasoning. We have many, many, many constructs in Dart that are redundant with other constructs. The argument that Y is redundant, so we can make it do something else doesn't hold any weight with me. Because now you have syntax that looks like it does Y, but does something unrelated, which is X.

So, trying to evaluate which behavior is worthwhile for us to put into the language:

  • If we make ? participate in null-shorting, it gives users a way to express something useful that they cannot easily express otherwise. So, it's worthwhile because it increases the expressiveness of the language.
  • If we make ? not participate in null-shorting, then the resulting behavior is identical to something they can already express. And the way they can already express that behavior is, I think, clearer and better in every respect.

My point, which I have been making very consistently, is that this may be necessary, but it is not sufficient. I agree with the above: making null-shorting participate is a more useful choice. However, this is not sufficient to make me think that this is the right choice, because I believe that making such a choice makes the language harder to read, and harder to write. And so I believe that it is better not to provide this convenient shortcut. Users who want this behavior will have to write their code differently. The result will be less convenient to write, but much more likely to be correctly interpreted by a reader.

@leafpetersen
Copy link
Member

someone else could also write something?.potentiallyNull! and intend it to only document that potentiallyNull is non-null.

This seems very stretched to me. You're saying "I have some code. It can clearly evaluate to null. The receiver of that code is set up to handle null. But I want to go ahead and throw an exception if this expression evaluates to null in one particular way, but not another". I mean, sure.. it's possible that someone wants to write this. But I simply don't believe that it is a common use case here. And in fact, the one single empirical case we have of this code being written in the wild, does not match this case.

@jakemac53
Copy link
Contributor

I do agree with Leaf that we should design the behavior based on what is most intuitive to the average reader, and not what is the most expressive/useful to a sufficiently informed writer.

What I don't agree about is that the non-null shortening behavior is intuitive. The little evidence we do have actually suggests the opposite if anything.

Ultimately, all the evidence that I have seen so far tells me that both ! or !. in this context are highly confusing and a large percentage of users will interpret it the wrong way regardless of what behavior is chosen. So because of that, I would be inclined to simply not allow it to be written. That way there is no ambiguity or misunderstanding.

We already know that it is a rare scenario anyways, and you can use parenthesis to get the non-null shortening behavior, which while a bit more verbose is very clear as to the intention. The null shortening behavior is more verbose, but it will also be very explicit.

Making it an error also buys us the time to do proper user studies in the future, if desired, to make sure we get the behavior right. Or we just leave it as an error if nobody complains.

@leafpetersen
Copy link
Member

What I don't agree about is that the non-null shortening behavior is intuitive. The little evidence we do have actually suggests the opposite if anything.

I really hate to keep harping on this, but this is simply not true. We have three solid examples of people encountering a null shorting expression ending in !: two informal users studies that I did, and one natural experiment referenced above. In all three of these cases, the user expected the non-null shorting behavior.

It is also true that a small majority of users that we've asked about this think that !. should null-short. And that when asked about ! in conjuction with a !. example a small majority of users has come down on the side of null shorting either the second or both.

This is why I keep distinguishing between ! and !..

I am therefore, at this point as comfortable as it is possible to be given the limited evidence in saying the following:

  • We have decent evidence that the behavior a user encountering e! will most likely expect is that the expression does not evaluated to null.
  • We have decent evidence that many (but not all users) would expect that a !. which follows a ?. will participate in the shorting.

I understand that those two data points lead to opposite conclusions.

@jakemac53
Copy link
Contributor

jakemac53 commented Aug 27, 2020

I really hate to keep harping on this, but this is simply not true. We have three solid examples of people encountering a null shorting expression ending in !: two informal users studies that I did, and one natural experiment referenced above. In all three of these cases, the user expected the non-null shorting behavior.

But in the informal survey 60% of respondents preferred the opposite. Yes, that survey does not present the two cases in isolation, but it does have a lot more respondents and from more typical users (and I also don't think it is a bad thing necessarily for them to have to consider both cases when forming an opinion).

@leafpetersen
Copy link
Member

But in the informal survey 60% of respondents preferred the opposite. Yes, that survey does not present the two cases in isolation, but it does have a lot more respondents and from more typical users (and I also don't think it is a bad thing necessarily for them to have to consider both cases when forming an opinion).

I did address this in my comment. The question of whether a user, when prompted to think about the semantics of !., extrapolates that to the semantics of !, is a fundamentally different question from whether a user when encountering a use of ! with no prompting will intuit the same semantics. The two data sets are addressing completely disjoint questions, and so the sample size is essentially irrelevant for comparison purposes.

I did mention this when the survey was being written: it would be a much more effective survey if users were not presented the questions together, but rather were randomly assigned to either be prompted first or not, with no opportunity to change their previous answers. Such a survey would give some insight into how people think about e!. But as designed, it really, really doesn't.

@lrhn
Copy link
Member

lrhn commented Aug 27, 2020

The question of whether a user, when prompted to think about the semantics of !., extrapolates that to the semantics of !, is a fundamentally different question from whether a user when encountering a use of ! with no prompting will intuit the same semantics

Nobody is going to be exposed to only one of those. Users won't have to intuit the meaning of expressions from scratch each time, they will learn about both, and how they read things then is more important than first impressions.

And I believe a null-shortening participating ! is more consistent overall, because seeing ?. makes you look from there to the right, past all selectors and postfix operations, to find the end. Seeing a ! only makes you look to the selector right before it. At least, that's the behavior I'd like to teach to users, when we teach them how the new and fancy shortening ?. works.

@lrhn
Copy link
Member

lrhn commented Aug 31, 2020

We are getting closer to a deadline, but no closer to agreement, so I propose that we:

  • Make a trailing ! selector following a ?. or ?[...] selector a compile-time error.
  • If possible, make a !.foo or ![...] selector following a ?. or ?[...] selector participate in the null shortening.
  • If we don't have time for that, or we can't get agreement, make it an error too.

A trailing ! selector is either an expression of one of the forms

<constructorInvocation> <selector>*
<primary> <selector>*

or a <cascadeSectionTail> of the form:

 <selector>* 

(no assignment after), where the last selector matched by <selector>* is !,
and it is an error if any of the prior selectors of <selectors>* are one of:

`?.' <identifier>
`?' `[' <expression> `]'

(an <assignableSelector> which is not an <unconditionalAssignableSelector>).

Making a !.foo or ![ an error too would mean that any ! selector with a null-aware selector before in the selector chain is an error, not only trailing ones.

This postpones the decision about what to do.

It's still possible to do a non-participating trailing ! using parentheses, (foo?.bar)!.
It's possible for users to introduce participating cast using something like:

extension NonNull<T extends Object> on T? {
  T get nonNull => this as T;
}

We won't be locked into either behavior prior to being able to decide which one we want.

This proposal is based on the assumption that it's easier to introduce an error than it is to change existing behavior, and therefore more likely that we can do so in time.

@lrhn
Copy link
Member

lrhn commented Sep 3, 2020

Decision: We'll ask the implementors which approach is most expedient:

  • Make ! always participate in null shortening.
  • Make ! be a compile-time error when continuing a null-shortening
  • Make a trailing ! continuing a null-shortening a compile-time error, but an internal !. or ![ participate.

dart-bot pushed a commit to dart-lang/sdk that referenced this issue Sep 25, 2020
Bug: dart-lang/language#1163
Change-Id: I6e648f76413c5036a867b2125359f28eb03027c2
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/163727
Reviewed-by: Brian Wilkerson <[email protected]>
Commit-Queue: Konstantin Shcheglov <[email protected]>
eernstg added a commit that referenced this issue Oct 14, 2020
…1162)

Discussions about this decision can be found in #1163.
@leafpetersen
Copy link
Member

I believe this is done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
nnbd NNBD related issues
Projects
None yet
Development

No branches or pull requests

6 participants