Skip to content
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-aware subscripting use ?[ or ?.[ syntax? #376

Closed
stereotype441 opened this issue May 28, 2019 · 135 comments
Closed

Should null-aware subscripting use ?[ or ?.[ syntax? #376

stereotype441 opened this issue May 28, 2019 · 135 comments
Labels
nnbd NNBD related issues

Comments

@stereotype441
Copy link
Member

The draft proposal currently extends the grammar of selectors to allow null-aware subscripting using the syntax e1?.[e2], however we've had some e-mail discussions about possibly changing this to e1?[e2], which would be more intuitive but might be more difficult to parse unambiguously.

Which syntax do we want to go with?

@stereotype441 stereotype441 added the nnbd NNBD related issues label May 28, 2019
@munificent
Copy link
Member

var wat = { a ? [b] : c };

Is this a set literal containing the result of a conditional expression, or a map literal containing the result of a null-aware subscript?

I think we'll probably want to do ?.[]. Also, the cascade is ..[], so this is arguably consistent with that.

@leafpetersen
Copy link
Member

I believe the suggestion that @bwilkerson made was that ?[ is parsed as single token, and ? [ is parsed as two. So for your example:

var set = { a ? [b] : c };  // Set literal
var map = { a?[b] : c}; // Map literal

Note that Swift and C# both use ?[]. Swift seems to be able to correctly disambiguate between conditional expressions and null aware subscripts, but doesn't seem to use tokenization to do so.

    var x : Array<String>?;
    var t1 : String? = x?[0]; // Treated as a subscript
    var t2 : String? = x? [0]; // Treated as a subscript
    var t3 : Array<Int>? = x == nil ? [0] : [3];  //Treated as a conditional expression
    var t4 : Array<Int>? = x == nil ?[0] : [3];  //Treated as a conditional expression

@lrhn
Copy link
Member

lrhn commented May 29, 2019

Swift does seem to use tokenization to distinguish, it's just whether there is a space between the x and then ? which matters, not between ? and [.

Whether to trigger on x?, ?[, or even x?[, without whitespace should probably be determined by where we want to break lines.

var v1 = longName?
   [longExpression];
var v2 = longName
   ?[longExpression];
var v3 = longName?[
  longExpression];

I can't see any one to prefer. So, what do we do for ?.?

var v4 = longName
    ?.longName();

That does suggests that we want ?[ to be the trigger, for consistency.

C# does not have the issue because [...] is not a valid expression.

@jodinathan
Copy link

I think the question mark aways close to the variable as subscript is better to read.
x?

@munificent
Copy link
Member

munificent commented May 29, 2019

Leaf and I spent some time talking about this at the whiteboard. My take on it going in is that both options have some things going for them:

foo?[bar]:

  • Follows C# and Swift [EDITED: Kotlin doesn't have this operator]
  • Terse
  • Mirrors !: foo![bar]

foo?.[bar]:

  • Mirrors cascade: foo..[bar]
  • Mirrors other null-aware method syntax: foo?.bar()
  • Avoids the nasty ambiguity in: { foo?[bar]:baz }

We spent a while trying to come up with ways to avoid the ambiguity with ?[. A couple of them are probably workable, but none feel particularly great to me. In particular, relying on whitespace can really harm the user experience. In theory, it's not a problem in formatted code. But many users write un-formatted Dart code as an input to the formatter. And that input format would thus become more whitespace sensitive and brittle in this corner of the language. So far, those kind of corners are very rare in Dart, which is a nice feature. (The one other corner I recall offhand is that - - a and --a are both valid but mean different things.)

We talked about eventually adding null-aware forms for other operators: foo?.+(bar), etc. If we do that, we'll probably want to require the dot, in which case requiring it for subscript is consistent with that future.

Another addition we have discussed for NNBD is a null-aware call syntax. If we don't require a dot there, it has the exact same ambiguity problem:

var wat = { foo?(bar):baz }; // Map or set?

So whatever fix we come up with for the ?[ ambiguity, we'll also have to apply to ?(.

Finally, Leaf wrote up an example of chaining the subscript:

foo()?[4]?[5]

To both of us, that actually doesn't look that good. It scans less like a method chain and more like some combination of infix operators. A little like ??. Compare to:

foo()?.[4]?.[5]

Here, it's more clearly a method chain. Communicating that visually is important too, because users need to quickly understand how much of an expression will get null-short-circuited.

Putting all of that together, it seems like the ?.[ form:

  • Avoids ambiguity problems. (The lexer already treats ?. as a single "null-aware" token.)
  • Extends naturally to a null-aware call.
  • Extends to other null-aware operators.
  • Leaves Dart a more robust input language to the formatter.
  • Actually looks pretty OK in a method chain.

So we're both leaning towards ?.[. If users ask why we do a different syntax from Kotlin and Swift, I think it's easy for us to show the ambiguous case and explain that it's to avoid that.

@leafpetersen
Copy link
Member

leafpetersen commented May 31, 2019

@lrhn I'm going to close this in favor of ?.[ since I think I was the only one still on the fence and I think I've moved into the ?.[ camp now. If you've come around to ?[, feel free to re-open for discussion.

@lrhn
Copy link
Member

lrhn commented Jun 1, 2019

LGTM. I did not find the white-space based disambiguation tecniques convincing, they didn't fit well with the current Dart syntax, and I couldn't see any other reasonable way to disambiguate.

@DaveShuckerow
Copy link

Question: was any consideration given to the syntax map[?index] ?

This is simpler to remember (IMO) than map?.[index] and appears to avoid the ambiguity problem of wat = {map?[index]:value}.

@lrhn
Copy link
Member

lrhn commented Jun 28, 2019

The map[?index] notation looks misleading. Reading it, I'd assume that it is checking whether the index is null, not the map.

(On the other hand, that could be a useful functionality by itself: If a function parameter or index operand starts with ?, then if it is null, all further evaluation of that call is skipped and it evaluates to the short-circuiting null value. Since a call or index operation is inside a selector chain, it could have the same reach as ?., and we wouldn't need something new. Probably doesn't work for operators, though.)

@leafpetersen leafpetersen reopened this Aug 20, 2019
@eernstg
Copy link
Member

eernstg commented Aug 21, 2019

@lrhn wrote:

If a function parameter or index operand starts with ?, then if it is null,
all further evaluation of that call is skipped and it evaluates to the
short-circuiting null value.

When that idea was discussed previously, the main concern was that it would be hard to read:

var x = ui.window.render((ui.SceneBuilder()
        ..pushClipRect(physicalBounds)
        ..addPicture(ui.Offset.zero, ?picture)
        ..pop())
    .build()),
};

How much of the above should be shorted away when the picture is null? An option which was discussed was to put the test at the front:

var x = let ?thePicture = picture in e;

This would cancel the evaluation of e entirely when thePicture is null. With that, there wouldn't have to be a conflict with map[?index] as a null-shorting invocation of operator [].

But I agree that a null-shorting semantics for map[?index] would be confusing, and I'd still prefer?.[.

@Marco87Developer
Copy link

Marco87Developer commented Aug 27, 2019

Personally, I prefer ?.[.

@bean5
Copy link

bean5 commented Aug 27, 2019

Definitely ?.. Be willing to be different than other languages. Be built well from the ground up. If you want to be like the other languages there's no point in having another.

@morisk
Copy link

morisk commented Aug 27, 2019

  • In all (I am a were of)human languages question mark already include dot, this is just a repeating. Another redundant keystroke was removed.

  • Typing longer chaining would be an annoyance.

  • Things can get weird when chaining with ?. and ..

  • a[index] converted to a?.[index] looks wrong as a.[index]

  • Function nullability look completely wrong with myFunc?.()

  • Make Swift and C# developers at home could be a good goal. (I don't write C# just because of its weird pascal case notation) Being different is not necessarily a good thing here.

I am in favor of a?[index] and myFunc?()

@spkersten
Copy link

spkersten commented Aug 27, 2019

I find the arguments for ?.[] not very convincing.

Mirrors cascade: foo..[bar]

It behaves differently from cascading (types of the expression are type of foo vs type of foo[bar]) so I'd say it should not mirror it.

Mirrors other null-aware method syntax: foo?.bar()

I'd say it doesn't mirror this. Call syntax is foo.bar(). Making it null-aware adds just the question mark after foo, so mirroring this would be foo?[bar].

those kind of corners [operators where white space matters] are very rare in Dart

I found white space matters in Dart for: --, ++, &&, ||, !=, ==. Which are some very common operators, hardly a "corner".

foo()?.[4]?.[5] Here, it's more clearly a method chain.

But it is not a method chain, why should it look like one? bar[1][4] doesn't look like a method chain either.

In my opinion, the syntax should be consistent (adding a single ? to make something null-aware, instead of sometimes a ? and sometimes a ?.). Whether something "looks good" is personal and will probably change once you get used to the syntax.

@lrhn
Copy link
Member

lrhn commented Aug 27, 2019

(Edit: Kathy said this all this better already: https://medium.com/dartlang/dart-nullability-syntax-decision-a-b-or-a-b-d827259e34a3)

The current plan is to go with ?.[e] as null-aware index-operator invocation (and ?.[e]=... for setting, and potentially ?.(args)/?.<types>(args) as null-aware function invocation).

A null aware cascade will be e?..selectors which means that we have e?..[e2] in the language already.

This syntax parses without any ambiguity, whether we require ?. to be one token or two.
(We have not decided on that, it might be useful to make it one, but it may also disallow some formattings that others might want to do, like have x? on one line and .foo() on the next).

The alternative proposed here is to use e1?[e2] as null-aware index lookup. I agree that it could be easier on the eye, the arguments against it are mainly of concerns about complication of parsing and writing.

This does not parse unambiguously if ? and [ are treated as two tokens because {e1?[e2]:e3} parses as both a set literal and a map literal. So, if we try this, we will need some disambiguation, and it seems very likely that we'll have to treat ?[ as a single token, and ? [ as two tokens. (The other option is to check for space between e and ? in e?[...] vs e ?[...], which is unprecedented in Dart).

If we treat ?[ as a single token, then e ?[ e2 ] is a null-aware index lookup.

Currently you can write text?[1]:[2] and have the formatter convert it to text ? [1] : [2]. With a ?[ token, the formatter couldn't do that. We have other composite operators where inserting a space changes the meaning, but currently the only one where breaking the operator into two is still valid syntax is prefix --, and there is no use for - -x, so that doesn't matter in practice. All other multi-char operators would be invalid code with a space inside them, but both ?[ and ? [ could see serious use, so we raise the risk of accidentally writing something else than what you meant by omitting a space.

The ?[ operator would not work as well with cascades where e?..foo()..[bar]=2 is a null-aware cascade on e. It only checks e once. That makes it e?[foo] for direct access, but e?..[foo] for cascade access, not e..?[foo] as you might expect.

If we use ?[ for indexing, we should also use ?( for null-aware function invocation. That has all the same risks of ambiguity.

So, the arguments against ?[ is not that it doesn't look better (whether it does or not), but that the consequences and risk for the language are larger than for ?.[, and the benefits are not deemed large enough to offset that.

@spkersten
Copy link

@lrhn For clarity: Maybe too subtle of a difference, but my argument isn't about look better, but about consistency: If a user knows that foo.bar() can be made null-aware by adding a question mark after the possible-null-expression to make it foo?.bar(), their first try for foo() and foo[4] will be foo?() and foo?[4]. Of course you could say that the rule is "insert a question mark but make sure there is at least one period after it" and maybe that is only slightly less intuitive and good enough, but the article you're refer to asked for feedback, so I'm giving it :)

@shortercode
Copy link

Just a few semi on topic thoughts...

Comparing to other languages I can think of a couple of sticking points using "?." for optional chaining ( although neither of these cases actually clash with the proposed null aware subscripting operator ):

Implicit member expressions in Swift

let color: UIColor = condition ? .red : .blue

Numbers in JS, without the integer component

let value = condition ? .1 : .2

Referring to the "?[" option, I feel like the it would be possible to parse and differentiate from a ternary conditional. Do a speculative expression parse after a detected "?" token and then check if the token following the expression is a ":". It requires that you can rewind the token stream to a point prior to the speculative expression parse, and that if it failed there would be no side effects. I don't know enough about the structure of the Dart scanner/parser to say how feasible that it is but it seems like a lot of potential work.

In terms of plain personal preference I think I'd prefer ?[. As @spkersten says it's more intuitive. Realistically I think people will live with either, if ?.[ is less ambiguous to parse then go for it.

@gazialankus
Copy link

gazialankus commented Aug 27, 2019

Sorry, my initial reaction is to support ?[. You have spent so much effort on this and are in a much better position to decide of course, but I will share my point of view. Hopefully it could be useful.

I think the decision to go with ?.[ feels too much like a system-centric approach rather than a user(programmer)-centric approach. Why the language is more complete and proper etc. etc. would repeatedly have to be explained to the average programmer who goes "what the heck is that extra dot for, am I not just supposed to add a question mark to protect against dereferencing a null?"

I think the main selling point of ?.[ seems to be the "is this a set or map?" example. I'm sure programmers would be happy memorizing one way the other, just like they memorized the operator precedence order. They could go "oh you can't just put a question mark like that because it sticks to the nearby nullable type, use a paranthesis there if you want to make it a ternary operator". And if they are coding somewhat responsibly and are not using dynamic everywhere, the IDE would warn them that it's a Map and not a Set. To have this ambiguous example be a critical bug you have to be coding irresponsibly anyway. Therefore, removing this ambiguity feels more like a theoretical exercise rather than a practical solution.

The second strongest argument, the congruence with cascade syntax is not that convincing to me either, because it's easier to remember "you always add a question mark after the nullable" rather than "you also have to add a dot after the question mark, because it has to look similar to cascade (which is not what we are using here, but it needs to look similar)". The dot feels like it came out of nowhere.

The third, chaining: if I am chaining an operator like this, I am probably already treading lightly that I might be making a mistake somewhere. If my life depends on it, I am probably using a number of final intermediary variables anyway. If not, since chaining already made me careful and nervous, I can probably correctly use the dotless operator with a little more of an effort. If I want to make it look nice, I can add whitespace.

Either case, thank you for introducing non-nullable types! It's a huge step forwards and I won't really mind the final decision here 😄.

@bean5
Copy link

bean5 commented Aug 27, 2019

Hmm. I have a feeling that Dart is worry about this because it is used heavily by Flutter which is heavily used with Firebase. NoSQL engines and non-existent fields are so common it isn't even funny. If NoSQL is going to persuade your choice, please make it apparent that it is a key use case. Not saying it is, but if it is, I'd like to know.

I am having second thoughts against ?. now that I look at ?. ..

Here's an idea: build a survey with code snippets paired with potential results. Ask the user what they think they do. Let the results guide you. If it simply gets too confusing to use in any scenario (ex: ?. ..), then let's consider something else.

@bean5
Copy link

bean5 commented Aug 27, 2019

Maybe ?/. or /^$/ (not quite correct regex, but understandable by users of regex). Admittedly, that is too cumbersome and long to type. Maybe ?$ or ?^ in memory of it. You could take it a step further and have one assert non-null!

@bean5
Copy link

bean5 commented Aug 27, 2019

How about a superset symbol? It implies that the left is a superset of the right. Empty anything isn't really a superset of anything, so it would work, right? :

myVar⊇[index]. Since it isn't on most keyboards, you'd want a 2-character equivalent: myVar=> (a bit confounding with >=. Maybe one of these would work ?> or ?>=. Its starting to look like garble, and like javascript (ex: ===). While I'm here, I might as well try to exhaust the search result space: !?0, ?|. ?%.

@munificent
Copy link
Member

I'd say it doesn't mirror this. Call syntax is foo.bar(). Making it null-aware adds just the question mark after foo, so mirroring this would be foo?[bar].

Fair point. What I had in mind is that it mirrors treating [] as another kind of null-aware method call. Null-aware method calls start with ?., so doing ?.[ would match that. We don't currently support calling operators that desugar to method calls using method call syntax like Scala does. In Scala, you can write a + b or a.+(b) and they mean the same thing. We've discussed supporting that in Dart. (Idiomatic code would use the normal infix syntax, but this notation can be handy for things like tear-offs, or embedding an operator in the middle of a method chain.)

The idea here is that if we were to do that, then using a?.[b] for the null-aware subscript call would match a.[b] for the unsugared notation for calling the subscript.

foo()?.[4]?.[5] Here, it's more clearly a method chain.

But it is not a method chain, why should it look like one? bar[1][4] doesn't look like a method chain either.

It is a method chain. The [] operator in Dart is just another kind of method call syntax. This is important because null-aware operators will short-circuit the rest of a method chain, so it's important for a reader to easily be able to tell what the rest of the method chain is so they understand how much code can be short-circuited.

This syntax parses without any ambiguity, whether we require ?. to be one token or two.
(We have not decided on that,

Are you sure about that? If I run:

main() {
  String foo = null;
  print(foo ? . length);
}

I get compile errors.

it might be useful to make it one, but it may also disallow some formattings that others might want to do, like have x? on one line and .foo() on the next).

The formatter already handles splitting on null-aware method chains and it keeps ?. together. (It basically has to since the ?. is a single token in the analyzer AST. I'd have to do a lot of work to allow splitting it.)

If a user knows that foo.bar() can be made null-aware by adding a question mark after the possible-null-expression to make it foo?.bar(), their first try for foo() and foo[4] will be foo?() and foo?[4].

Yeah, unfortunately using foo?.[bar] means we don't have a rule that simple. We're sort of stuck with the history of already having a ternary operator and the ambiguity that that causes. We have to route around that by having a less regular syntax for null-aware subscript operators.

@morisk
Copy link

morisk commented Aug 28, 2019

If we must use additional un-ambiguity characters maybe we can consider ?? instead of ?..
a = foo??[index]
b = myFunc??()

@lrhn
Copy link
Member

lrhn commented Aug 28, 2019

@morisk
The ?? operator already exists in Dart and foo??[index] is already a valid expression (with a list literal as second operand). Using it for null-aware indexing would be a breaking change.

"All the good syntaxes are taken!"

@eernstg
Copy link
Member

eernstg commented Dec 16, 2019

Right, point well taken.

@lhk
Copy link

lhk commented Dec 16, 2019

Oh, I guess I hadn't thought this through. @Cat-sushi , thanks for pointing it out.

I'm sorry, but I no longer understand the position of the dart team here. @eernstg says:

So it's definitely fair to say that switching to nnbd will cause breaking changes, and nearly everybody will need to make some changes to their code.

@munificent says:

This makes NNBD a non-breaking change. Your existing Dart 2.x code will continue to run fine without change even after NNBD ships.

So, what is it going to be? If you have managed to make NNBD a seamless transition where existing code doesn't have to be touched, then I understand the necessity of ?.[. But if the code has to be touched in any case, I think it is very viable to expect people to run dartfmt on their codebase. And then I would assume it to be easy to just switch out the ternary operator for something less conflicting.

@eernstg
Copy link
Member

eernstg commented Dec 16, 2019

It will not break existing code when imported libraries switch over to NNBD, but if you want to port your own code to NNBD then it is very likely that various parts of it needs to be updated.

@lhk
Copy link

lhk commented Dec 16, 2019

It sounds as if you will basically have two 'compilation modes'. If your code doesn't contain any NNBD syntax, it is compiled with the legacy mode. Then as soon as you start using the NNBD syntax, the compiler expects consistency and you will have to adapt your code.
Is this correct?

In that scenario, why would it be a problem to change the ternary operator? Dart with and without NNBD code is handled separately in any case, with breaking changes to the syntax.

I always feel like there are times for discussion and times for sticking with a decision and making it work. It seems to me that I'm late to this issue and that the decision has been made sometime in spring. Please excuse me if I'm wasting your time here. I do very much appreciate the efforts of the dart team and their push towards NNBD.
That being said, I also think that most people will be taken aback by the inconsistency of ?.[. And it sounded to me as if you would be quite open to moving the ternary operator syntax out of the way, if only it wouldn't require refactoring (comment by @leafpetersen ):

@lhk Not it! :) I'd be happy to have different syntax for conditional expressions, so long as someone else is volunteering to manage the migration of millions of lines of code.

Well now it sounds like managing this migration is really no problem at all. Millions of lines of code where you have to think about the proper new type for your int x; variables, that's a timesink. Running dartfmt on code that you have to refactor in any case, to magically swap out ? for | (or whatever syntax you prefer) will take no time at all.

@eernstg
Copy link
Member

eernstg commented Dec 16, 2019

Language versioning makes it possible to indicate that a particular library is at a specific level. This can be used to say that the library isn't yet ready for nnbd. It is possible to have opted-in as well as opted-out libraries in the same program. You could consider this to be two modes, but they are not permanent, they are just used to get from a completely pre-nnbd state to a completely nnbd state without forcing everyone to do it at the same time.

This transition has been used to introduce a lot of breaking changes (e.g., --no-implicit-casts). However, we shouldn't add so many breaking changes that it gets impossible to start using nnbd...

PS: I'll stay out of the discussion about ternary operators. ;-)

@leafpetersen
Copy link
Member

Well now it sounds like managing this migration is really no problem at all. Millions of lines of code where you have to think about the proper new type for your int x; variables, that's a timesink. Running dartfmt on code that you have to refactor in any case, to magically swap out ? for | (or whatever syntax you prefer) will take no time at all.

Yes, we could unquestionably tack this onto the NNBD release (or any other large opt-in breaking change that we manage via language versioning). Pragmatically, even if we had consensus on this now and had all of the technical details worked out (I'm not sure we do) I don't think it's feasible to add this to the task list for the NNBD release at this point. I'm highly sympathetic to the desire to FIX ALL THE THINGS NOW, but we really need to ship NNBD and move forward.

@leafpetersen
Copy link
Member

By the way - as a meta-level comment, I really appreciate both the style and the content of the discussion here. There are a lot of insightful comments, and some good "outside of the box" suggestions that I've found useful to think through. So thanks for that!

@munificent
Copy link
Member

It sounds as if you will basically have two 'compilation modes'.

Right. You can think of the Dart SDK as simultaneously supporting two separate languages "legacy Dart" and "NNBD Dart". You can write your code in either language and it will run both of them just fine. Your program can even be a mixture. Sort of like "strict mode" in JS.

If your code doesn't contain any NNBD syntax, it is compiled with the legacy mode. Then as soon as you start using the NNBD syntax, the compiler expects consistency and you will have to adapt your code.
Is this correct?

We don't implicitly opt you in to the new NNBD flavor of Dart by detecting your attempt to use it. Instead, you have to opt in your package by updating the SDK constraint in your pubspec to require a version of Dart that supports NNBD. But, otherwise, you have the right idea.

We don't consider this a breaking change because your existing code keeps working just as it does today. If you want to use the NNBD features, you have to opt in to NNBD and that may require you to change other parts of your program. But that's something you choose to do when you want to choose to do it. We don't break your code.

@Cat-sushi
Copy link

@munificent

We don't consider this a breaking change because your existing code keeps working just as it does today. If you want to use the NNBD features, you have to opt in to NNBD and that may require you to change other parts of your program. But that's something you choose to do when you want to choose to do it. We don't break your code.

AFAIK, the migration period during which legacy code without NNBD opted in and new code with NNBD opted in can run simultaneously is finite, and the Dart team will encourage all the developers to modify all their pieces of code especially those depended by others into those migrated with NNBD opted in, as soon as possible, in order to take full advantage of null safety. Because null safety even in code with NNBD opted in would berak around the border of legacy code, and also because the SDK can't make optimization which should be made in applications with NNBD fully opted in. In addition, at some point after the migration period, I guess the SDK will drop legacy mode, just like the SDK dropped the mode which could be called "Weak Mode" at Dart 2.0, in order to make the SDK simple again. At that point, the lifetime of applications and libraries which have decided not to be migrated will end.

Needless to say, removal of current syntax of conditional expression a ? b : c, if it will happen, will have to be opted in under // @dart = 3.0 or so, or it will be just deprecated and remains for a while.

@leafpetersen
Copy link
Member

leafpetersen commented Dec 18, 2019

The language team met this morning, and we spent some time reviewing this issue. There are no concrete changes in the outcome at this point, discussion is ongoing. Here's my quick summary of the discussion points.

  • There is general agreement that we can make ?[ work technically if we wish to

    • If we do this, we would almost certainly choose to tokenize ?[ as a single token, rather than rely on the whitespace between the receiver and the ? (Swift takes the latter approach)
    • There is some general distaste for adding white space sensitivity, since it means that auto-formatting has to preserve certain whitespace choices in order to avoid potentially changing the users code.
    • There is a general concern around the long term opportunity cost of further overloading the syntax (in terms of out ability to add future language features).
    • Neither of the previous two concerns is large enough to be decisive: that is, we can live with them if we think ?[ is the right thing to do.
  • The remaining question then is should we use ?[ vs ?.[, ignoring issues of feasibility.

    • There is concern that it adds cognitive load for users, since they may accidentally write ?[a] in locations where the intention was not to have it parsed as a subscripting (either conditional expression, or possible future features like null aware collection elements), and they will have to decipher error messages to understand that they need to write their code as ? [a].
    • There is concern about generalizing this. Specifically:
      • We will almost certainly want to add a null aware call operator. Whatever we choose for null aware subscript, we will almost certainly want null aware call to be the same. That implies that we either have a?[b] and f?(x, y) and f?<T, S>(x, y) or we have a?.[b] and f?.(x, y) and f?.<T, S>(x, y). We would almost certainly have to resort to the same tokenization hack to avoid ambiguities here as well (e.g. {f?(x):y}).
      • There is some desire to have method call forms for operators. That is, to be able to write a.+(b). The natural null aware form of this would be a?.+(b). It's slightly odd that this is asymmetric with the subscript operator.
      • Generalizing to tear-offs may or may not be a future issue, and should be consistent. Would a?[] work acceptably?
    • The symmetry between a![b] and a?[b] is appealing.
  • Calling out specifically the aesthetic choice.

    • There is some weak preference for the ?.[ choice within the team, particularly when viewed in the context of method chains.
    • There is a general sense that the majority of developers that we have engaged with about this are either neutral or in favor of ?[ - there doesn't seem to be much of a contingent with a strong active preference for ?.[.

@Cat-sushi
Copy link

they will have to decipher error messages to understand that they need to write their code as ? [a].

The operator '[]' isn't defined for class 'bool' is OK for me.

@Cat-sushi
Copy link

The expression doesn't evaluate to a function, so it can't be invoked is also OK for me.

@leafpetersen
Copy link
Member

Generally, we can say that for any op, we support a.op(...) - and, by implication, a?.op(...).
WDYT?

Good point.

@lhk
Copy link

lhk commented Jan 5, 2020

I didn’t comment further, because I felt like I had voiced my opinion and didn’t have anything new to contribute.

But now curiosity is taking over :P. Is there some progress regarding this discussion?

Also, I would like to say that I appreciate how much you respond to the community. Thank you for investing the time to go over this again. Even if the outcome doesn’t change, it feels like you very much valued our feedback.

Happy new year :)

@leafpetersen
Copy link
Member

But now curiosity is taking over :P. Is there some progress regarding this discussion?

I hope to have a decision one way or the other very shortly.

Also, I would like to say that I appreciate how much you respond to the community. Thank you for investing the time to go over this again. Even if the outcome doesn’t change, it feels like you very much valued our feedback.

Thanks! And as I said above, we very much appreciate the high quality feedback we've received, and the thoughtful and respectful tone of the discussion.

Happy New Year to all you language enthusiasts out there! :)

@munificent
Copy link
Member

munificent commented Jan 21, 2020

TL:DR: We're doing ?[.

OK, friends! I want to thank all of you for all of the very very helpful feedback.

A clear take-away from this thread was that almost everyone prefers the ?[ style and our arguments that ?.[ is cleaner or simpler in some abstract grammatical way were not compelling enough to sway that preference. That's good to know.

The question remaining for us was, if we were to do ?[, how should we resolve the ambiguity? We spent a lot of time on this thread here and elsewhere talking about various options and we have one now that we're happy with. So the resolution of this issue is that, like most (all?) of you prefer, we'll use ?[ for null-aware index operators.

The mechanism we'll use to resolve the ambiguity is basically, "if it is syntactically a valid conditional expression, then it is parsed as one". So in this example:

var what = { a?[b]:c };

The a?[b]:c is parsed as a conditional expression and you get a set literal.

Conditional expressions are always preferred even in cases that technically aren't ambiguous. So here:

var mustBeMap = <int, String>{ a?[b]:c };

In this case, the <int, String> means that there is no real ambiguity, since the element in there must be a map entry, not an expression. Even so, we still treat a?[b]:c as a conditional expression and then report an error because that's not a valid map entry.

The goal here is to avoid parsers and—more importantly—human readers needing to take into account too much surrounding context in order to read a piece of code. We don't want you to have to scan back and say "Oh, this must be a map literal, so actually even though it does kind of look like a conditional expression, it's not."

In practice, what this means is if you don't want a conditional expression, you need to parenthesize the key:

var mustBeMap = { (a?[b]):c };

I think cases where this comes into play are likely to be very rare anyway. A null-aware index operator can evaluate to null and how often do you want a map whose key is null? When it does come into play, I think the parentheses help it stand out so the reader doesn't accidentally misread it as a conditional expression.

The nice thing about this approach is that it doesn't make parsing whitespace sensitive. That means you can still throw unformatted code that hits this ambiguous case at dartfmt and it will be able to make sense of it.

When we started this thread, I honestly expected most people wouldn't care one way or the other and those who preferred ?[ would be easily convinced that ?.[ is better for pragmatic reasons. Instead, I am now convinced that ?[ is a better syntax for us and for you all. Thanks for helping us make Dart better!

I'm going to go ahead and close this issue now since we've reached a decision (and one I believe most of you will be happy with), but do feel free to comment if you have further thoughts and if need be we can reopen.

@kwalrath
Copy link
Contributor

kwalrath commented Mar 5, 2020

@leafpetersen I still see ?.[] in the proposed spec. Is there an issue to update that?

@leafpetersen
Copy link
Member

@kwalrath #874

@droidluv
Copy link

droidluv commented Apr 3, 2020

Can't wait for this to be out, I rather work with default than extensions like

extension MapExtension<K, V> on Map<K, V> {
  V getValue(K k){
    if(this == null) return null;
    return this[k];
  }
}

Was trying to replicate kotlin, so I can do someMap?.getValue("key"), besides in languages like kotlin a nullable type cannot access index with [] has to be ?.someFetchMethod()

@angelhdzmultimedia
Copy link

¿Any date or Dart version where this is gonna be implemented, guys?

@eernstg
Copy link
Member

eernstg commented May 5, 2020

This is part of the upcoming null-safety feature set. For instance, you can run the following on https://nullsafety.dartpad.dev/:

class A {
  Object operator [](int i) => true;
  operator []=(int i, Object? o) {
    print("Setting $o");
  }
}

main() {
  A? a = A();
  var b = a?[0];
  a?[0] = b;
}

@angelhdzmultimedia
Copy link

This is part of the upcoming null-safety feature set. For instance, you can run the following on https://nullsafety.dartpad.dev/:

class A {
  Object operator [](int i) => true;
  operator []=(int i, Object? o) {
    print("Setting $o");
  }
}

main() {
  A? a = A();
  var b = a?[0];
  a?[0] = b;
}

Thank you very much!

@aktxyz
Copy link

aktxyz commented Sep 4, 2020

is this supposed to work on dartpad now ?

void main() {
  var a = ['1','2','3'];
  print(a[0]);
  print(a?[4]);
}

@lrhn
Copy link
Member

lrhn commented Sep 4, 2020

@aktxyz No, use nullsafety.dartpad.dev for null safe code until null safety is released (and dartpad updated).

@Cat-sushi
Copy link

Cat-sushi commented Sep 5, 2020

Even at nullsafety.dartpad.dev, that will cause a warning, because a is a List<String>, but not a List<String>?.

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