-
Notifications
You must be signed in to change notification settings - Fork 205
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
Should null short-circuiting short-circuit spreads? #330
Comments
We might also just stick to the explicit approach, and say that In return, Dart would avoid having a situation where the (I believe that |
To be mildly pedantic, implicitly adding class A {
dynamic foo() => null;
}
void test() {
A? a = new A();
print(a?.foo().isEven);
} evaluates to a null pointer error, rather than printing out null (which is what you would get if you just inserted a |
I think so, yes, strongly. I believe there is a clean narrative we can make for null-shorting which is "the scope of the shorting is the surrounding expressions that have null-aware forms". So, in If that narrative appeals, we have some work to do to make it actually true:
I like this narrative, and I think 1 and 2 are pragmatically useful, so I think we should null short spreads as well as adding those other null-aware operators. An equivalent narrative is: "If not shorting some operation X would cause a type error (because some null-aware subexpression has a nullable type in a position where a nullable type isn't allowed) and there is a null-aware variant of X, then X should null-shorted."
It should insert no elements in the surrounding collection. That's what users want. Your second desugaring is what I would want, but [...(l != null ? l.map((x) => x+1) : const [])]
The spread syntax is a little odd because it reads right-to-left, but I don't think "null shorting propagates to the right" is the clearest mental model. Instead, think of it as "null shorting propagates up the syntax tree". That's true both for method chains and for spreads, it's just that the root of the tree is textually on the left for spreads and the right for methods. There are other places in the language where execution goes right to left, like in So the idea is that a null-aware operator evaluating to null in certain subexpression positions may cause certain parent expressions to be skipped as well.
Yeah, sorry I mentioned that in the language meeting. That's not the right way to look at it. Sorry for the confusion. |
Shouldn't short circuit be handled on the spread operator? [...?l.map((x) => x+1).where((x) => x > 1)];
// same as
[...?l?.map((x) => x+1)?.where((x) => x > 1)]; |
@leafpetersen wrote:
I'd certainly expect |
I would personally say that if null-awareness short-circuiting extends to spreads, then [... l?.map(x) => x+1).where((x) => x > 1)] should be valid. If Short circuiting is not equivalent to just inserting extra A (If you think of the problem using continuations, then a short-circuiting is a delimited continuation, so C2(C1(e)) (an expression with the continuation C2∘C1) becomes |
@lrhn wrote:
OK, that approach allows This means that we're considering null-awareness propagation as a semantic rather than a syntactic mechanism, hence the continuations. But this kind of specification cannot be used directly (unless we want to start talking about 'the expression evaluation completes with null-propagation', and that's surely more general than needed/appropriate). The problem is that it is ambiguous (depending on what The current grammar allows for expressions of the form But this means that we'd have to say something like "and (If we do not add the "biggest expression" constraint then we would be allowed to rewrite So this means that We could consider If we use that desugaring then we'd automatically be able to skip over constructs that do not have a nullable form today, e.g., But all these things don't change the fact that the skipping actions are always going left-to-right. So I still tend to prefer an explicit [...? work.hard().to[3].getToA.listWithASomewhatLongerName?.map((x) => x+1).where((x) => x > 1)] The |
Importantly, only if
Yes. I have a proposed semantics (given as a source to "mostly source" transform) in the NNBD spec proposal. |
I don't think so. The principle that we're going by is that we want to take things that would otherwise just always be a static error requiring you to add a bunch of Your interpretation is more like: |
I'm leaning towards not including the spread operator in the null-propagation context. I'm sure it's doable, but I think it might also be confusing to users. It is already potentially confusing that you can do It does mean that you have to write |
Yes, I'm considering a semantic model where "the expression evaluation completes with null-propagation" is included. We may want to introduce shorthands like "evaluate an expression to a null-aware value" vs "evaluate an expression to a value", where the former can evaluate to a "propagating null" and the latter would evaluate the same expression just to Or maybe we won't need that because our grammar is of the form Take Selector evaluation of no selectors on v evaluates to v.
(similarly for selectors |
I'm worried about that too, but my hunch is that it's practically useful enough that we'll regret not doing it. If we don't any time you use a null-aware operator somewhere inside the operand to a spread, you'll have to change the That sounds an awful lot to me like the original motivation for doing null-shorting in the first place, so...
I have always found this corner of the grammar very odd. Semantically, the receiver is a subexpression of the invocation, so the "natural" grammar in my mind is:
Do we not specify it that way because we want to avoid left recursion in the grammar, or is there some other reason? |
@munificent There is the problem of cascades. A I guess we could have two different productions, one for normal invocations and one for cascade-content invocations, so the above becomes For on-cascade invocations, I think it's only a matter of not duplicating productions unnecessarily. |
@munificent wrote:
I think that might very well be the main reason. The form you mention is more compositional (it supports specifying the meaning of a term in the smallest possible increments) whereas the form But the specification already specifies the meaning of various terms of the form |
@lrhn wrote:
We have the same syntactic structure for cascades as we do for selectors, However, it is not very meaningful to have In each cascade there could be occurrences of For example, let v = foo in v == null
? null
: (let v1 = v, _ = (let v2 = v1.bar in v2 == null ? null : v2.baz) in v1) which might of course be further simplified because we're testing several times whether the same value is null, and because the variable As before, |
The "problem with cascades" that I was eluding to here is that I can't find a nice compositional grammar for the individual selector chains. That's hardly surprising with the way cascades work. Allowing The desugaring of And yes, the assignment is also correct, let v= e in if e == null ? null : let _ = (let v2 = v.x in v2 == null ? null : v2.y=e1) in v |
This never got resolved. Reading over the comments, I think @munificent is in favor, I'm not sure where @lrhn landed. Thoughts on this? |
Still against it. (Which is also why we should fix it so that |
@eernstg is also not in favor of this. @munificent do you want to make a strong pitch for this, or can we mark this is as not planned? It should be something we can change our minds on later, since adding it would only allow more programs. |
If we use this as justification, is it going to limit us in what other null-aware forms we can add later? |
I don't know if I can make a principled argument for why we should do it, but I still think we should do it. If a user writes As far as I can tell, that argument applies equally well to I think if we don't do this, users will keep tripping over it and asking why we don't do it. (In fact, we've already gotten users asking why spreads don't implicitly skip null even without a null-aware operator.) Here's the potential user experiences as I see it:
Looking at those, it seems like short-circuiting is useful and maybe a little delightful for the users who expect it, and not particularly harmful for those who don't.
Yes, it would. I think this came up in the language meeting when we talked about it. :-/ |
I spent some time talking to @natebosch about this and I think I've been persuaded to the other side. The two things I found most compelling are:
Given that, and that the rest of the language team seems to be on the side of not shorting, I think that's probably the right choice, at least for now. |
Closing: We do not introduce null-shorting for the spread operator. The nnbd spec has reflected this decision for quite some time already. |
As part of the NNBD language feature, we will allow null-aware operators to "short-circuit" a certain amount of the their continuation. So for example,
a?.b.c
will evaluate tonull
ifa
evaluates tonull
, instead of throwing a null pointer exception whenc
is called onnull
.Should we extend this behavior to spreads? And if so, what semantics should it have? That is, given:
If short-circuiting does not propagate through spreads, this is a compile time error, because
l?.map(...)
evaluates to either anIterable
ornull
, and it is an error to spread a nullable type. So you would have to change the code to...?l?.map((x) => x+1)
.If we make null-short-circuiting propagate through spreads, then this code could be allowed.
There is a question as to what semantics to give it. Consider
...l?.map((x) => x+1)
.if (l == null) null else ...l.map((x) => x+1)
if (l == null) ...?null else ...l.map((x) => x+1))
The latter semantics seems likely to be more useful.
The text was updated successfully, but these errors were encountered: