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?
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.
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
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.
I think the question mark aways close to the variable as subscript is better to read.
x?
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]:
!: foo![bar]foo?.[bar]:
foo..[bar]foo?.bar(){ 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:
?. as a single "null-aware" token.)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.
@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.
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.
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}.
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.)
@lrhn wrote:
If a function parameter or index operand starts with
?, then if it isnull,
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?.[.
Personally, I prefer ?.[.
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.
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?()
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.
(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.
@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 :)
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.
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 馃槃.
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.
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!
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, ?|. ?%.
I'd say it doesn't mirror this. Call syntax is
foo.bar(). Making it null-aware adds just the question mark afterfoo, so mirroring this would befoo?[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 itfoo?.bar(), their first try forfoo()andfoo[4]will befoo?()andfoo?[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.
If we must use additional un-ambiguity characters maybe we can consider ?? instead of ?..
a = foo??[index]
b = myFunc??()
@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!"
I've come from the article in Medium, but I'm not convinced by the reasoning in the article, because,
.+ is currently illegal, and the introduction of .+ or ?.+ looks unnatural, so the abrupt introduction of ?.+ doesn't justify the introduction of ?.[ or ?.(, well.?. and ?.. don't introduce additional dot, so those can't be good reasons to introduce ?.[ or ?.( with additional dot, either.a[1][2][3] is already a method chain intentionally omitting dots, so a?[1]?[2]?[3] must be acceptable regardless of method chains.With respect to the formatting result for a?[e1]:e2, it doesn't matter when a formatted non-NNBD code a ? [e1] : e2 is being migrated, providing that ?[ is space sensitive. In addition, it seems acceptable that a new NNBD code is formatted to a?[e1]: e2, because it is human readable enough to be checked, and syntactical/ type checking reveals most of human errors, whether in braces or not.
Consequently, I prefer ?[, ?( and ?+ much more than ?.[, ?.( and ?.+.
To avoid the ambiguity, I think ?[ and ?( should be inseparable tokens, but I'm not sure the correct solution, anyway.
Self-commenting,
I think
?[and?(should be inseparable tokens, but I'm not sure the correct solution
Tokens such as --, ++, &&, ||, !=, == ... are always space sensitive, so ?[ as a space sensitive token is unexceptional to be acceptable.
If ?[ is a token, then a?[e1]:e2 is not ambiguous at all. It is just a little confusing for persons who have particular mental model, but the confusion might be mitigated by the formatter.
Having said that, it could be a problem for the formatter, when the formatter have to treat both of NNBD code and non-NNBD code. Even if so, I hope the temporal problem doesn't restrict the future of the language. I mean the formatter should ignore unformatted conditional expressions especially as members of set literals which might be broken silently with a NNBD version of dart.
To assess the impact of the ignorance, I'd like to know a statistic how many codes have unformatted conditional expressions as members of set literals. I hope the number is not so big, because set literal is a quite new feature and the formatter is widely used.
I would like to go with e1?.[e2]
It's handy enough, looks more concise to me and architectural more correct as the alternative. It has more technical correctness, as there are several good arguments for that. The argument of the alternative is, on average, that the arguments of the former are not overriding arguments.
Going for NNBD should give a clear and concise syntax, which is e1?.[e2] in my eyes.
@n8crwlr What do "concise" and "clear" mean?
As a fact, ?[ is shorter than ?.[, and ?[ resembles [ better than ?.[ does.
When I fast look to my?.['foo']?.['bar'] I automatically think that there is some default function being called after the dot or something like that.
I would try hard to keep the ?[ syntax.
@Cat-sushi These are not points for me. As written, ?.[ is handy enough and very concise, (especially in NNBD code) so just saving a . is a belief and not a necessity. We are talking about _nullable types_ in _not nullable by default_.
If i look _fast_ into my code, seeing my?.['foo']?.['bar'], yes, i would like to think that's calling on nullable type. I really do not feel better with my?['not']?['functions'] - that's start of nested ternary? The alternate is ok, but it is not 'yeah, good decission guys!' At least, say _these are functions_ is very far fetched for me.
I've understood the point of @n8crwlr is
I really do not feel better with my?['not']?['functions'] - that's start of nested ternary?
As I said, it is little confusing for persons who have particular mental model, but the formatter might mitigate the problem, because the formatter always puts spaces around ? and : of ternary expression. In addition, the first operand of ternary operator should be a boolean expression, on the other hand, in most case, the fist operand of subscription is a identifier who's name stands for List and is not suspicious to be a boolean expression.
More importantly, I guess that most persons don't have such mental model that they have to put additional dot just after null aware mark ?, because there already are null aware operators such as ?. (method invocation) and ?... (spreading), which don't introduce additional dots.
@Cat-sushi I am not talking about the tools. I am talking about me. I like ?.[. It's far easier to read and easier to spot with the eye even in large sources.
I am not sure where your problem is if you do not like it. Dart will change to NNBD, what means, only those types in your code are affected, which _must be nullable_ in your implementation.
I knew ?.[ is easier to read for you, but I guess ?[ is easier to write and read for most persons including me.
I couldn't understand what you want to say with the second paragraph of your last comment. I knew NNBD well.
I might miss it, how does simple nullable work? ex final a = foo?baz if yes, then how
foo?baz?.[42]?bar is not looking more confusing or meaningless or language not consistent?
@morisk Sorry, I couldn't understand your question. What final a = foo?baz and foo?baz?.[42]?bar mean? Could you write non-NNBD versions of those?
@Cat-sushi
@n8crwlr wrote:
?.[ is handy enough and very concise, (especially in NNBD code)
There is a description Issue-155 to write nullable chainning using ?. therefor using ?.[ is indeed more concise with the NNDB.
Personally i'd prefer ? chainning like in other languages I use.
@morisk
I think we have reached the consensus that c?.m1().?m2() is the future of the method chain, where m1 and m2 are regular methods. I also agreed with ?. which doesn't introduce additional dot in comparison with c.m1().m2().
We are now talking about special case of method chain with [] such as a[e1][e2], and the point at issue is which is better a?[e1]?[e2] or a?.[e1]?.[e2].
I prefer a?[e1]?[e2] as you do.
edited
@Cat-sushi
I prefer a?[e1]?[e2] as you do.
Yep.
?.[ is handy enough and very concise, (especially in NNBD code)
I am not native english speaker. Concise in my meaning says: clear to read / strong visibility.
A dot is one of most used operators, there is no problem to type it in some nullable types. As said, it is handy enough for me.
?.[ is rather ugly IMO, but likely unavoidable.
But what about an alternate solution instead:
Currently, we almost always want foo?.bar?.baz instead of foo?.bar.baz. So what about having syntax sugar for the former, which would implicitly solve this issue?
Instead of foo?.bar?.baz, we could write ?.foo.bar.baz.
Which means that instead of foo?.[0].baz we'd have ?.foo[0].baz
@rrousselGit
issue-155 discusses that with respect to o?.m1()?.m2(), the second ? is omittable providing that o?.m1() is null if and only if o is null, in other words, return type of m1() is not nullable.
Your proposal is independent from issue-155 or this issue.
I prefer ?[ over ?.[ because:
foo?.bar and foo?[1]All these talks about "ambiguity" make sense for internal stuff. But I, as a user of language, want to know less about internal decisions which affect public API and concentrate on solving my domain/app problems.
Currently all arguments toward ?.[ look more like excuses. I personaly don't think that the case when you write something like { a?[b] : [c]} is so popular that should be solved on language level. I would be more tolerant to this syntax if I saw performance problems rather than problem with set or map.
However, I highly appreciate your openness to the community, work and other tehnical decisions so I'm totaly could live with solution ?.[ :)
Thanks!
All these talks about "ambiguity" make sense for internal stuff. But I, as a user of language, want to know less about internal decisions which affect public API and concentrate on solving my domain/app problems.
This is a fair goal, but the point is that the ambiguity isn't something that is "internal" to the implementation of the Dart language. The grammar is the "public API" of the language, so if it's ambiguous, it's broken.
I can try to explain in terms of an analogy. Imagine you maintain some library package "foo" that has lots of users. You want to add a new public function to it called doStuff(). Unfortunately, "foo" already has a different public function with that same name. You can't simply say "well, the existence of that old function is an internal implementation detail of foo". It's part of the public API. And, very concretely, you can't simply have two functions with the same name. The tools can't handle it.
Grammar ambiguities are the same way. We simply can't say that one single piece of syntax means two different things at the same time. The parser has to do something and whatever it picks changes how the user's program behaves. This is a user-facing choice.
@munificent
Generally speaking, I totally agree with you, but we would like to have a practical discussion.
I would like to have statistics which shows how much programs have such code as {a?[e1]:[e2]}.
Also, I would like to know options to avoid the ambiguity other than ?.[.
I would like to have statistics which shows how much programs have such code as
{a?[e1]:[e2]}.
We don't have numbers, but my hunch is the numbers are very low. But for a grammar ambiguity, that doesn't really matter. We simply can't have an ambiguity. The parser must choose one way or the other, so any ambiguity must be resolved even if never encountered in real world code.
I would like to know options to avoid the ambiguity other than
?.[.
The very top of the issue discusses this some. The other option we looked at was making whitespace sensitive such that ?[ and ? [ are treated differently by the tokenizer. That resolves the ambiguity by saying that if you intend {a?[e1]:2} to be a set, you must put a space after the ? and if you want it to be a map, you cannot. But this also means that whitespace would be meaningful everywhere ?[ is used, even in unambiguous cases.
That in turn makes the language more error-prone for users and code generators that are producing Dart without wanting to be careful about whitespace. A very common programming style in Dart is to just pound out a bunch of code without regard to whitespace, and then run dartfmt and let is sort it out. That style becomes riskier every time we make the language more dependent on whitespace for parsing a program.
Likewise, many Dart code generators produce code without worrying about whitespace and let dartfmt fix it up. Those code generators will need to be written more carefully if a space between ? and ? is meaningful.
The alternative syntax, ?.[ doesn't have these problems. The only thing it really has against it is that some users don't like the way it looks. That matters, of course, but given that some users do like the way it looks, that isn't an obvious deciding factor.
@munificent
The only thing it really has against it is that some users don't like the way it looks.
I don't think that the problem in how it looks but which mental model it creates. When I see foo?.bar and foo?[bar] I treat ? as a separate operator but which works with conjunction with access operators. Here I should remember only one operator "?" and the rule that I can use it with access operators. But when I see foo?.bar and foo?.[bar] I should remember two operators which logically behave identically. Yes, I still should remember two things but now these two things not general and not composable as in the previous example because in the previous example I had only one operator and rule which allows to use it with any access operators.
so instead to remember:
? - null-aware
. - object property access
[ - array item access
I should remember:
. - object property access
?. - null-aware object property access
[ - array item access
?.[ - null-aware array item access
Yes, I still should remember two things but now these two things not general and not composable as in the previous example
Yeah, I agree completely. It's not as simple, regular, and composable as it could be.
At the same time, if we wanted complete regularity, we'd probably eliminate [ ] entirely. It's just another syntax for calling a specially-named method. For example, Scala and Smalltalk don't have any special syntax for subscripting.
When you're designing a syntax, one of the key goals is to minimize the amount of new things a user has to learn before they can be productive. There are two main approaches:
One of the primary challenges of language design is that these two points are often in conflict because the languages many users already know are themselves not simple, orthogonal, and composable. Dart's syntax is based on JS and Java, which are in turn based on C++, which is based on C. There's a lot of weird historical baggage and irregularity in there. But it's baggage millions of users have already internalized so Dart is easier for them to learn if we adopt it.
Also, we're shackled to our own history. It's very hard for us to making breaking changes to the language. So new features sometimes don't slot in as gracefully as we'd like because we don't have the luxury of pushing around existing features to make room for them.
The end result is that syntax design is often a matter of making trade-offs. In this case, using ?.[ is a little special and irregular. It's another thing a user has to learn. But, it's probably better than them having to learn that ?[ and ? [ mean two entirely different things to the language. And, if we ever allow other operators to be called with . syntax like Scala does (1.+(2)), then this irregular subscript syntax will become more regular because then it will follow the other operators.
I agree with a lot of that. I don't think you should copy other languages
just be more learnable, unless you take the good from various languages.
Just copying one defeats the purpose of having another language and just
makes yet another language.
Also, you mention regularity. Do you mean this in terms of language
regularity or being a "regular language" (English is not)? I never learned
that a regular language can become more regular, but I suppose Dart is not
100% regular because of [ ]. So it makes sense that it can become more
regular. It is a good point. I just always thought of languages as either
regular or not rather than on a continuum like entropy is.
@munificent
That resolves the ambiguity by saying that if you intend
{a?[e1]:2}to be a set, you _must_ put a space after the?and if you want it to be a map, you cannot. But this also means that whitespace would be meaningful everywhere?[is used, even in unambiguous cases.
I think it's not a problem for most persons.
That in turn makes the language more error-prone for users and code generators that are producing Dart without wanting to be careful about whitespace.
I don't think so because of 3 reasons.
{a ? [e1] : [e2]} is a very rare case.a?[e1]:[e2] outside of a Set literal is marked as error by the analyzer.? of ternary operator.A very common programming style in Dart is to just pound out a bunch of code without regard to whitespace, and then run dartfmt and let is sort it out.
Yes, I agree to it.
At the same time, if we wanted complete regularity, we'd probably eliminate
[ ]entirely. It's just another syntax for calling a specially-named method. For example, Scala and Smalltalk don't have any special syntax for subscripting.
Isn't It an extreme argument? [ ] is super popular from C lang to Dart, in the latter it's just a syntax sugar of method invocation.
There's a lot of weird historical baggage and irregularity in there. But it's baggage millions of users have already internalized so Dart is easier for them to learn if we adopt it.
The end result is that syntax design is often a matter of making trade-offs.
I understood this is the reason why this issue and the article are posted, and I would like to vote to ?[.
Would it be horrible to propose that linters should warn users that the following is ambiguous unless they clarify by typing the left side?
var set = { a ? [b] : c }; // Set literal
var map = { a?[b] : c}; // Map literal
I suppose using the left to disambiguate is possible, but probably not a great plan. It looks like good, safe code, when in reality you have an edge case where you _have to specify type_ in a loosely typed language.
So this proposal seems dead on arrival, but I figured I'd mention it just in case. Dart was born in the age of IDEs and is gaining momentum in great part because of Flutter, which seems heavily tied to modern IDEs with tons of plugins, so perhaps it is a viable approach, although cumbersome and counter-intuitive when learning from the problems that languages had in the past. Even if we avoid the linter approach for null-aware subscripting, it is only a matter of time before we face higher-level questions that perhaps we start to lean on linter help and farther-reaching disambiguation techniques. Who knows.
I don't know if any of you noticed, but I'm on the fence, which is why I've voted for both sides here.
Admittedly, I'd rather have it in some short form rather than talking about it into oblivion by inaction. Flutter seems to be progressing very quickly, but this feature is sitting. Every second counts. Every second gives other languages time to compete.
Ultimately, my intuition is that we should just pick ?.[. Not because it is not ambiguous or because I think it is good choice, but because _it seems to contain more information and would be forwards compatible if we decide to change it in a future version_. That'll buy us time to try it out. Starting with the other would mean not being able to automatically shift to whatever we ended up running with in the long run. We won't know until users start to file complaints. I don't foresee many complaints since I think the audience for this will be power users. Novices will have to learn it only as quickly as it becomes mainstream in everyday use.
That being said, I've had peers complain when I use the ternary operator because "it is an advanced syntax" therefore decreasing maintainability. It is only advanced because we don't expect the newbies to learn it and because we don't use it often enough. It is self-fulfilling prophecy I hope to avoid this time around with Dart. _Let's build for the future, not the past._
Also, you mention regularity. Do you mean this in terms of language regularity or being a "regular language"
In the general English sense of "internally consistent", not in the formal language sense of "non-recursive grammar.
after following the TC 39 optional chaining discussions, i've got a serious D茅j脿 vu, but they summed their proposal up in a pretty concise faq: https://github.com/tc39/proposal-optional-chaining#faq i guess it's not the worst outcome for ECMAScript and dart optional chaining to look similar..
Thank you for the TC39 link. It is very relevant to Dart because both languages have C/Java style syntaxes.
I think the only thing in the FAQ that I disagree with is that I do want long short-circuiting. Dart already has a notion of "chain of applications" from cascades, and it also makes a good delimiter for short-circuiting null-aware operators.
i've got a serious D茅j脿 vu, but they summed their proposal up in a pretty concise faq:
Yes, it's quite similar, and it isn't convincing any more than the article in Medum.com or pro-?.[ comments here.
As for
Can anyone fill the blanks of
<some construct not supported by X or working differently in X>
The answer here is usually "C-style conditional operator and map/object/set literals", I believe.
@munificent Aren't there any languages who have all of C-style ternary operator
What is the e1?e2:e3, Set literal with braces {e1,e2,e3}, Map literal with braces and colon {e1:e2} and null-aware subscripting with ?[ or ?.[ other than Dart?
My understanding of Set literal in languages.
Correct the mistakes.
Dart: {1, 2, 3} // does have ternary operator e1?e2:e3
Python: {1, 2, 3} // doesn't have ternary operator
Swift: [1, 2, 3] // Array literal can be used as Set literal in context of Set)
Kotlin: N/A // instantiated by constructor
Rust: N/A // instantiated by constructor
JavaScript: N/A // instantiated by constructor
Go: N/A // doesn't have Set
Is this a issue specific to Dart?
Aren't there any languages who have all of C-style ternary operator
e1?e2:e3, Set literal with braces{e1,e2,e3}, Map literal with braces and colon{e1:e2}and null-aware subscripting with?[or?.[other than Dart?
The only one I know of offhand that has similar syntax is JavaScript, and they are also going with ?.[.
My question was that Dart is the only language who have all of ternary operator like e1?e2:e3, Set literal like {e1} and Map literal like {e1:e2}, which cause the ambiguity?
The proposal of ?.[ for JavaScript is at stage 3, anyway.
JavaScript is a mess, people escape to other languages like Dart and Go.
Please be better then TC39.
My question was that Dart is the only language who have all of ternary operator like
e1?e2:e3, Set literal like{e1}and Map literal like{e1:e2}, which cause the ambiguity?
It's the only one I know of offhand. Ruby and Python have similar literals, but no ternary. Java and C# have ternary but no similar literals.
JavaScript is a mess, people escape to other languages like Dart and Go.
While I sympathize with the general sentiment, every language has many useful things we can learn from it. And, whether we may like it or not, many users coming to Dart will know JavaScript, so we lower the amount of new things they have to learn if we follow JavaScript in places where it makes sense to do so.
many users coming to Dart will know JavaScript
Hopefully the majority of people will come from iOS, Android, Xamarin. They all use ?[
I prefer the '?[' notation (and also '?+' with other operators).
I know this is a tough decision but to me a language is a tool. And what we look for, when using a tool, is efficiency. To achieve that a language need to be as concise, readable and predictable as possible and the '?[' is closer to that than '?.['.
To summarize: the only "bad" point of using '?[' is whitespace awareness ? It just doesn't allow us to use it with a whitespace right ?
To me it's by far better than having to add an unnatural dot between. I don't see the big deal in that.
In language, dots remind us of a method call. Our brain is used to that. Breaking with this habit and having to type an extra special character is a bit disappointing.
If there is no other limitation beside the whitespace sensitive issue (which is a VERY small one to me), I'd love you consider this option.
Edit: if other null-aware operators can't be implemented with the same notation for technical reasons I'd rather keep things consistent. So either '?[' and '?+' OR '?.[' and '?.+' but I'd be curious on what are the limitations for the operators.
To summarize: the only "bad" point of using '?[' is whitespace awareness ?
I think we on the language team also slightly lean towards preferring the look of ?.[, especially when it appears in a method chain (which is likely to happen for things like traversing JSON). In something like:
json?["some"]?["property"]?["chain"] ?? defaultValue;
I think it's easy to accidentally read that as conditional operators or if-null (??) operators mixed in with list literals. The ?[ doesn't look like a single token to my eye, especially given that there are cases where those two characters appear near each other today and are not a single token, as in: condition ? [someList] : another.
With:
json?.["some"]?.["property"]?.["chain"] ?? defaultValue;
To me, those . help it look more like a single method chain.
To achieve that a language need to be as concise, readable and predictable as possible and the '?[' is closer to that than '?.['.
True, but part of predictability is ensuring that aspects that users don't expect to be meaningful are not meaningful. I don't know if many users would predict that ? [ means one thing while ?[ means something completely different. Spaces are meaningful in some cases, like - - versus --, both those are rare and have decades of history.
In language, dots remind us of a method call.
Index operators are method calls. This is very important for users to understand because it affects short-circuiting. In Dart with NNBD, if the receiver of a null-aware method call or index operator evaluates to null, then the rest of the method chain gets short-circuited and skipped. Say you write:
foo?.a().b().c();
If foo is null, then Dart will skip not just a() but b() and c() too. This is equally true of:
foo?.[0].a().b().c();
If foo is null, the null-aware index operator is skipped, as is the rest of the method chain. So it's really important for users to be able to quickly identify what "the rest of the method chain" is when they look at some code. Using ?.[ helps reinforce that this is a method call, and one where this short-circuiting behavior is involved.
So either '?[' and '?+' OR '?.[' and '?.+' but I'd be curious on what are the limitations for the operators.
If we end up doing other null-aware operators, I believe we will likely use a dot, so a?.+(b), etc. Otherwise, we are opening up even more ambiguity problems. Also, one useful reason to support method call syntax for operators like this is because it could let you opt into the same null-aware short-circuiting behavior (which doesn't apply to infix operators by default). In that case, it would be useful to support forms like a.+(b). At that point, the . is mandatory because a +(b) is already valid syntax with established, non-short-circuiting behavior.
This makes sense, especially the chain part.
Thank you for taking time to explain.
Whatever you choose I'll be satisfied. I was just afraid you were thinking more from a language maker point of vue than from a user perspective.
I've understood the importance of method call syntax to short-circuit null aware symbol ? for arithmetic operators with whom the order of operations is not always left to right.
Now, The method call syntax of subscripting, if necessary, should be a.[](1), but I think it is not important, because chains of subscripting operators are always evaluated from left to right.
On the other hand, as a normal syntax of subscripting operation, a?[1] seems more straight forward than a?.[1], as I mentioned.
Likewise, both of a?.+(b) as a method call syntax and a ?+ b as an operation syntax should be acceptable.
I don't know if many users would predict that
? [means one thing while?[means something completely different.
I don't think so, because ? [ is necessarily followed by ] :.
And the widely used formatter always put space just after ? of ternary operator, as I mentioned.
Index operators are method calls.
It can't be a reason because foo[0][1][2][3][4] is already a popular method chain syntax sugar without dots, as I mentioned.
This is very important for users to understand because it affects short-circuiting. In Dart with NNBD, if the receiver of a null-aware method call or index operator evaluates to null, then the rest of the method chain gets short-circuited and skipped.
foo?[0]?[1][2][3]?[4] is OK for me, where the members of foo[] like foo[0][1] and the members of foo[][] like foo[0][1][2] have List<some non-nullable type> types.
Having said that, the problem of preference could not be solved without popularity voting.
On the other hand, the problem of mental model seems quite fixed game, described below.
Q: Fill the blanks of XXX, YYY and ZZZ. (Select the correct answer 1 or 2)
|a is non-nullable|a is nullable|
---|---
|a.b|a?.b|
|a..a|a?..b|
|a.b()|a?.b()|
|a..b()|a?..b()|
|[...a]|[?...a]|
|a[b]|aXXX[b]|
|a(b)|aYYY[b]|
|a + b|a ZZZ+ b|
?, YYY: ?, ZZZ: ? ?., YYY: ?., ZZZ: ?. To avoid repeated discussions, can anybody hopefully in Google summarize the above discussions?
Whatever the dart team chooses with convincing reasoning, I also will be satisfied.
I think Bob (@munificent) is doing a good work summarizing the trade-offs, which includes both existing syntax, potential future future syntax, visual appearance, etc., for example in https://github.com/dart-lang/language/issues/376#issuecomment-534793712.
Code like x?[x]?[y]: 42 is very hard to read. I know that if we go with ?[ always parsed a single token, the meaning is unambiguous (and I'll have to look further back for the ?, or if this is inside {...}, it's a map entry), and the formatter will help me by inserting spaces in reasonable places, but it is still not readable. A single ? simply has too much history as the conditional operator.
If we removed the ?/: operator entirely, say by using if (test) expr else expr as an expression, like we already kind-of do in collection literals, then the ? would be free, and I'd be less worried about the usability of ?[. I don't think that's realistic at the current time, though.
I had already read all the comments here, but I don't feel them convincing.
Even if the system simply makes <List<int>>{a?[1]:[2]} intending Set literal an error, do the system have to look back?
I don't care (much) about the complexity for the parser, as long as the grammar is not ambiguous.
I do care about the readability to users.
The compiler is always right. What it does is what the program means (because or compilers obviously have no bugs :smile:).
The trick with syntax is not to make it convenient to compilers, as long as it's unambiguous, the compiler will do its job. We can make that job more or less expensive, but it's rarely the bottleneck ... as long as your type system is not Turing complete, or something.
The real requirement of good grammar is that users who read or write code must understand it the same way as the computer. Anything that is hard to read for users, no matter how unambiguous it technically is, is a usability problem.
Having to look back too far is a problem for people, not compilers. It makes code harder to read.
So, good syntax means that users read the code the same way as the compiler.
I currently think that ?[ is too hard to read (aka. too easy to misread) for it to be good syntax.
That's mainly because a stand-alone ? already means something that is itself semi-hard to read, and the ? in ?[ does look stand-alone. I think ?.[ is easier to recognize as something distinct from the ? in ?/:.
Anything that is hard to read for users, no matter how unambiguous it technically is, is a usability problem.
I said that it is readable enough with the formatter for me.
But, I understand it is your strong belief that {a?[1]:[2]} is ambiguous.
I'm convinced, now.
One more thing to think about when it comes to syntax design: Each new syntactic form can be an _asset_ to us as developers, because we can now use a more expressive language and write more readable/powerful/concise programs; but it is also an _expense_ in terms of future language enhancements, because more syntactic forms means more sources of ambiguity. So we pay every time we add something that allows for syntactic forms that we might want to give some new meaning in a future extension: That future extension must then have a different syntactic form.
Even thought your claims just made me convinced that ?.[ is pretty much ok and lets move forward, I wonder, just for curiosity:
C# has the ternary ? condition and also non-nullable access through ?[, what does an experienced C# programmer has to say about this ambiguity in its everyday use of the language?
@eernstg
I understand that languages are not democracy, and I'm always ready to be satisfied with any decisions by the dart team.
But, I always seek good reasoning for controversial decisions to keep loving the language.
Now, I almost believe that ? is already occupied by null-aware somethings with exception of ternary operator.
So, I can't easily imagine a occurence of combination of ? and [ in a future enhance for whom the token of ?[, which is just lexical but not syntactical, would be a obstacle.
Could you illustrate some hypothetical examples, if you can?
C# has the ternary
?condition and also non-nullable access through?[, what does an experienced C# programmer has to say about this ambiguity in its everyday use of the language?
C# doesn't have map and set literals using {}, which avoids the ambiguity we have in Dart.
I currently think that
?[is too hard to read (aka. too easy to _misread_) for it to be _good_ syntax.
That's mainly because a stand-alone?already means something that is itself semi-hard to read, and the?in?[does _look_ stand-alone. I think?.[is easier to recognize as something distinct from the?in?/:.
I find it's still confusing for me to look at - the . makes it look like some sort of property resolution is about to happen.
Since we have to disambiguate, how about a ?? as the operator? This is similar to dart's ??= operator, which similarly short-circuits the assignment depending on the presence of a null operand.
While this deviates from JS, perhaps it's visually/semantically more coherent with Dart's other null-dependent operator?
(or does this create another ambiguity I haven't thought of??)
C# has the ternary
?condition and also non-nullable access through?[, what does an experienced C# programmer has to say about this ambiguity in its everyday use of the language?C# doesn't have map and set literals using
{}, which avoids the ambiguity we have in Dart.
I had to work with C# for a few months and, for so much time working with JS and dart, at a moment I forgot that C# didn't have map literals... conclusion: I've got myself into this ambiguity while trying to create some map and ternary condition.
That can, in fact, be annoying to figure it out.
I've switched to team ]?.
If it were for me to decide, I would approach the problem from another angle.
Dart has ternary operator ?: and recently gained collection if, that is kind of similar but not exactly. In my opinion having ergonomic non-/nullable types in a language is much more important than ternary operator that causes parsing ambiguities.
My proposal is: Let's deprecate both ternary operator and collection if and replace it with conditional expression if <condition> then <value> else <alternative> (or python style <value> if <condition> else <alternative>), that can be used both in expressions and collections.
The language would be much cleaner IMHO.
That would be more legible and expressive, but would be like ruby (readable if you want it to be).
Since we have to disambiguate, how about a
??as the operator? This is similar to dart's??=operator, which similarly short-circuits the assignment depending on the presence of a null operand.While this deviates from JS, perhaps it's visually/semantically more coherent with Dart's other null-dependent operator?
(or does this create another ambiguity I haven't thought of??)
Yes:
var list = [1];
print(list??[0]);
Does this print [1] or 1? :)
Would it be a good idea to support a functional form of operators? E.g. a.operator+(b) as a synonym for a + b? . Then we get the following form for free:
list?.operator[](0)
Another advantage: we get "operator tearoff" (resulting in a normal function):
var addAsFunction=a.operator+.
I would even go for list.[] and a.+, without the operator keyword, for tearing off the operator function. Because operator is allowed as a member name a.operator + b already has a meaning, but a.+(b) does not.
Now, if we also allowed parentheses-less application, f a instead of f(a), then a?.+ b would be the same as a?.+(b). That's probably going too far, so we may have to special-case a?.op b to works as a null-aware binary operator application.
Doesn't work well for unary-, sadly, because again a?.unary is already a valid member access.
Without the operator keyword, the syntax becomes too cryptic IMO.
Maybe add parens:
a.(operator+)
a.(operator unary-)
Foo.(constructor)
Foo.(constructor named)
a.(getter x)
etc?
Also: foo.(name) - returns string "foo"
In fact, you can define an entire new language inside parens :)
Here's a more concise but still (hopefully) readable version of the above:
a.(+) // operator tear-off
a.(unary-)
a.(get x) // getter tear-off
a.(set x) // setter tear-off
Foo.(new) // default constructor tear-off
Foo.(new named) // named constructor tear-off
foo.(name) // returns "foo"
Probably there are more uses, but the general principle is: the notation is used when we want to access some property that cannot be accessed with usual dot notation because of potential conflicts with existing syntax. (E.g. I remember there were more requests to add "properties" to types, which conflicted with static methods and/or constructors)
Here's a more concise but still (hopefully) readable version of the above
That proposal would probably be more well-placed in #216, or in a new issue proposing support for tear-offs of "everything".
Do I understand it correctly, this discussion is only about removing ambiguity arising from the existence of the ternary operator? If that operator didn't exist, there would be no issue with ?[ and then I guess that no-one would prefer ?.[over ?[.
The two desired syntaxes are mutually exclusive and so we are trying to come up with the second best to ?[?
In that case, I would argue that null checks are going to be used much more often than the ternary operator. So if something is compromised, it should be the ternary operator syntax.
I very much like the proposal of @altermark: if <condition> then <value> else <alternative>.
If that doesn't align with the C-Style of the rest of the language I'm sure that we can find other versions of the ternary operator, like:
?(<condition>){<expression>}{<expression>}
A more radical suggestion (and what I would prefer personally) would be to convert if-statements to expressions (let them return a value) and just deprecate the ternary operator entirely.
A more radical suggestion (and what I would prefer personally) would be to convert if-statements to expressions (let them return a value) and just deprecate the ternary operator entirely.
You can already use if-expressions - e.g. instead of var x = cond ? 0 : 1; you can write
var x= [if (cond) 0 else 1].first;
:-)
To introduce if-expressions in a form more convenient than that, dart needs a way to distinguish if-expression from if-statement. I think parentheses can help here, too: var x = (if (cond) 0 else 1);
Same with (much requested) switch-expressions and match-expressions.
I also like the idea of deprecating the ternary operator in favor of if-expressions.
I guess it wouldn't be very hard to create an executable that reads dart files of a project and turn ternary operators into if-expressions.
This is a tangent (consider opening a new request for if-expressions).
The main issues with having if (e1) e2 else e3 as a conditional expression is ambiguity with the conditional statement or collection element, either for parsing or when read by humans. And maybe an issue of with users expecting things to be different from what they are. And that it requires an else part.
Writing if (test) print("hello world") else print("not"); is one semicolon away from if (test) print("hello world"); else print("not"); The former is an expression statement with an conditional expression, the latter is a conditional statement.
You can change the latter to if (test) { var greet = "hello world"; print(greet); } else print("not");, but not the former. Would you expect to be able to? (One of the most common confusion point around collection literals is that users want to put bracers around conditional elements).
In a collection literal [if (test) e1 else e2] would need to be disambiguated because it can be either a conditional element or a conditional expression (we can always safely make it a conditional element, we just have to say that we do so, so the parser knows how to parse it).
I believe it to be possible to change e1 ? e2 : e3 to if (e1) e2 else e3 and still be able to parse it to mean the same thing.
In my opinion having ergonomic non-/nullable types in a language is much more important than ternary operator that causes parsing ambiguities.
IMHO, I agree with @altermark.
If it is a consensus, should the ambiguity for humans be even negligible?
@lrhn , Thank you for the explanation, I see the complications.
It seems to me as if nnbd is the next big thing after the support for extension methods. I appreciate that this is a hugely non-trivial feature to implement, but as far as I can tell, it is being worked on very heavily and not that far away into the future.
And this operator is quite an integral part of nnbd.
So, is there some internal consensus on how it should look? Does the team have a preference? Is there some status quo, which would be implemented, given no further input by the community?
A Summary of Discussions, so far (+ alpha)
P0: Legend of Pros for a?.[b]
a?[b])P1: if a?[b] is subscripting, {a?[b]:c} is mentally ambiguous. // The belief of the Dart team (look this)
Set literal with single element which is a ternary operation in which the second expression is a List literal with single element and the third expression could be a List.a?[b] is subscripting, {a?[b]:c} is grammatically ambiguous.?[ as an inseparable token make it grammatically unambiguous.--a vs. - -a is already space sensitive grammar.?. is applicable to other operators such as a?.+(b).a?.[](b).a?.*(b)?.+(c)?./(d) looks quite odd.?+, ?-, ?*, ?/, and so on also could be inseparable tokens.a.?[b].a?[ evokes starting of ternary operation which is confusing.a?[b], anyway.a?.[b]?.[c]?.[d] makes method chain perceivable.a[b][c][d], which also is a method chain, has taken root without problem.a?[b][c]?[d] doesn't seem a problem.a?.[b] resembles a?.b and a?..b.a?.b and a?..b don't intorduce additional dots.a?[b] without additional dot resembles better.I believe it to be possible to change e1 ? e2 : e3 to if (e1) e2 else e3 and still be able to parse it to mean the same thing.
Or perhaps allow 3-arg syntax if (e1, e2, e3) to mean the same thing? :)
Precedent: Visual Basic
So, is there some internal consensus on how it should look? Does the team have a preference? Is there some status quo, which would be implemented, given no further input by the community?
I think the description of our thinking here still pretty much sums up the internal consensus. There's been a lot of good discussion here, and it's great to get a sense of where people in the community fall, but I don't really see anything that changes the balance on the decision.
@leafpetersen I think the article is the problem which is misleading with unconvincing reasons P2, P4, P7 and P9 I showed here.
I think @Cat-sushi is reading my mind :P
Indeed, I read the article, felt entirely unconvinced and came here to see where the discussion stands.
Personally, I find the ?.[ syntax highly unelegant. As far as I understand it's a necessary evil, given the legacy of existing design decisions.
After reading the comment of @leafpetersen it sounds like this decision is pretty much made?
Out of curiosity: Has deprecating or restyling the ternary operator been on the table at some point, and if yes, why was it rejected?
If I understand the grammar concerns correctly, converting condition ? a: b to something like condition | a:b would also solve the problem.
And actually, I think this will even increase overall code readability. I always found the ternary operator kind of clunky to use. And with NNBD, question marks will be all over the place. Ternary operators will be even less readable then. If they have their own syntax, they would be easier to parse (for human readers).
I guess this is not possible since it would break existing code.
With respect to conditional expression, NNBD could be a good chance to break it with dartfix which fixes it, I think.
@munificent :
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 there's a way to disambiguate this without introducing new syntax.
This is a set literal containing the result of a conditional expression. To make it a map containing the result of null-aware subscript, we can write it as var wat = { (a ? [b]) : c };
That is, we are using a strandard disambuguation method - by adding parentheses. WDYT?
@tatumizer
We are talking about the spec of the language but not each piece of code.
@Cat-sushi : not sure what you mean by that. I'll try to reformulate my point: I think no changes are necessary in the spec other than allowing x?[expr] and x?(parameters). Maybe this conjecture is false, but I'd like to see a counterexample. And the counterexample is expected to come as a piece of code. (That's where the "pieces of code" come into play). Unless you meant something different :-)
@leafpetersen I think the article is the problem which is misleading with unconvincing reasons P2, P4, P7 and P9 I showed here.
@Cat-sushi To a first approximation I think we agree on the pros and the cons. We just don't agree on their relative weight, sorry! :)
Out of curiosity: Has deprecating or restyling the ternary operator been on the table at some point, and if yes, why was it rejected?
@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.
I think there's a way to disambiguate this without introducing new syntax.
@tatumizer There are lots of ways to disambiguate this - that's not the concern. The concern is users writing code that they think means X, but which the compiler interprets as Y.
@leafpetersen
The comment of mine doesn't describe priority at all, and I understand that the Dart team puts much weight on P1 and P3.
@tatumizer , I think the problem with your disambiguation is that dart has both set and map literals. The compiler will not raise an error if the user forgets to add the braces, it will simply parse a different type:
{"a": 1}; // map literal
{"a"} ; // set literal
{ (a ? [b]) : c }; // map literal
{ a ? [b] : c }; // set literal
If I understand @leafpetersen correctly, this is the kind of ambiguity (not for compiler but for human readers) he is talking about. Both the code with and without braces will be accepted by the compiler. But it means totally different things.
@tatumizer, @leafpetersen, For what it's worth, I would still very much prefer the approach taken by @tatumizer. Dart is a strongly typed language, so I don't think that this renegade set literal would get very far before causing a compiler error after all :)
This kind of mistake should not propagate very far and be easy to find.
As soon as your sets or maps have more than one entry, it should become an immediate compiler error in any case. Well, at least as long as the other entries are not based on the NNBD syntax and can be unambigously parsed.
I thought about the above problem of disambiguation set and map literals. It would be nice to have the linter point to an issue like this. But both versions are valid code, you would have to discard the linting messages somehow and I guess it's not acceptable to have something like a stateful linter that remembers "he told me this instance is ok".
The current spec allows this code condition?expr1:expr2. It looks horrible...
Maybe the following could be considered:
I would propose to deprecate the non-whitespaced ternary operator together with the introduction of NNBD. And to use the proposal of @tatumizer of the ternary operator simply having precedence over null-aware subscripting. Additionally, the linter would warn against non-braced versions of either the ternary operator or null-aware subscripting in map or set literals.
Now we have the following situation:
{"a": 1}; // map literal
{"a"} ; // set literal
{ a ? [b] : c }; // set literal, linter warning
{ a?[b] : c }; // map literal, linter warning
{ (a ? [b] : c )}; // set literal, linter happy
{ (a?[b]) : c }; // map literal, linter happy
Example 3 in the above code is a breaking change. It used to be a ternary operator, with NNBD it becomes subscripting. This changes the overall expression from a set to a map literal.
But I assume that dartfmt will format the ternary operator with whitespace in any case? So properly formatted code will not break. And it introduces a linter warning in any case, so the error case would not be silent.
However, I am aware that this has a huge disadvantage. Ternary operators in map or set literals now lead to a linting error (even if they are nicely whitespaced), since they are not surrounded by braces. I guess this would be unacceptable, since it floods existing code with linter warnings. Maybe dartfmt could also add these braces to ternary expressions. So we are back to: "properly formatted code will transition to NNBD seamlessly"
I posted #740 "If-expression without ? with mandatory else-part" in accordance with C1.2 in this.
@leafpetersen :
The concern is users writing code that they think means X, but which the compiler interprets as Y.
Oh I see. You mean, some real user, in a real program, writes var wat = { a ? [b] : c }; and expects it to evaluate to "a map containing the result of null-aware subscript"? The map is supposed to have a single entry keyed by an array. Quite possible. But we could probably agree on one thing: the author of the above program must be a truly original thinker. Would it be prudent for the language to try to optimize the syntax for such original thinker who may, or may not, exist? :-)
Much more likely scenario IMO is that a more down-to-earth user writes an innocent-looking expression x?[0] and suddenly gets an error and a recommendation to add a dot and gets very upset - the effect you already can observe in this exchange. :-)
@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.
@leafpetersen Eh, what about volunteering dartfmt for that job :P
It's not as if NNBD will be zero cost for existing code. People will have to touch their packages in order to profit from this change. So I think expecting to run dartfmt on existing code is not that much of a burden.
Currently, the ternary operator has a unique syntax. Isn't it possible to do a clean sweep and replace it with anything but the question mark?
I don't want to be the loud minority here. Please excuse my strong opinion. But I couldn't care less if the ternary operator becomes condition | expr1 : expr2, while I care strongly about ?.[
It's not as if NNBD will be zero cost for existing code.
It's not, but the language allows them to incrementally migrate to NNBD (and we are taking on a very large complexity and tooling burden to enable that). This makes NNBD a non-breaking change. Your existing Dart 2.x code will continue to run fine without change even after NNBD ships.
Removing the ternary operator means millions of lines of Dart code would have to be migrated or would stop working. That's a very unpleasant thing to do to the ecosystem, especially when all they get in return is a little syntax sugar.
Isn't it possible to do a clean sweep and replace it with anything but the question mark?
Unfortunately, in practice that's not how programming languages evolve. The primary value of a programming language implementation is that it can run a user's existing code. When an implementation stops running code users already have it very quickly ceases to be a useful tool.
Your existing Dart 2.x code will continue to run fine without change even after NNBD ships.
AFAIK, existing Dart 2.x code would be broken by NNBD, but dartfix will fix it to be migrated Dart 3.0(?) code with NNBD opted in. Isn't my understanding correct?
Is it bad idea that dartfix will migrate ternary operation in existing code to new expression without ? ?
Or, is it too late to do so?
@Cat-sushi no, the NNBD syntax will be backwards compatible and introduce no breaking changes.
Obviously, f({int x}) and int x; will break.
f({int x)){} have to be rewritten into f({required int x}){}, f({int x=10}){} or f({int? x}){} depending on the intention.
Seemingly, int x; have to be rewritten into late int x;, int x = 10; or int? x;.
@Cat-sushi wrote:
Seemingly,
int x;have to be rewritten
It can be handled statically in some situations, because it is OK for a local variable to have no initializer and a non-nullable type if it is 'definitely assigned' before it is evaluated. We only have to use late or make the type nullable in the case where the static analysis cannot prove the relevant definite assignment property.
Similarly, we recently made an adjustment that f({int x}); (that is: an abstract method) will not break: It is not necessary to specify a default value for a parameter of an _abstract_ method declaration, only a concrete declaration must have it.
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. But we're trying to keep the breaking changes as small as possible, and hopefully a large portion of the required changes will actually be improvements.
I new it.
My point was that NNBD is a breaking change.
Right, point well taken.
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.
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.
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.
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. ;-)
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.
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!
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.
@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.
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
?[ as a single token, rather than rely on the whitespace between the receiver and the ? (Swift takes the latter approach)?[ is the right thing to do.The remaining question then is should we use ?[ vs ?.[, ignoring issues of feasibility.
?[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].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}). 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.a?[] work acceptably? a![b] and a?[b] is appealing.Calling out specifically the aesthetic choice.
?.[ choice within the team, particularly when viewed in the context of method chains.?[ - there doesn't seem to be much of a contingent with a strong active preference for ?.[. 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.
Why is it asymmetric? You would still be able to write a.[](b) and a?.[](b). This involves tear-off (obviously, the expressions a.[], a.+ and alike are all tear-offs). For subscript, the language can support the form not involving tear-off: a[b] and a?[b]. I think it's all quite natural :-)
BUT: even if you don't want to treat a.op as tear-off, and instead consider a.op(...) as a single construct, still there's no reason to disallow a.[](...). Generally, we can say that for any op, we support a.op(...) - and, by implication, a?.op(...).
WDYT?
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.
The expression doesn't evaluate to a function, so it can't be invoked is also OK for me.
Generally, we can say that for any
op, we supporta.op(...)- and, by implication,a?.op(...).
WDYT?
Good point.
I didn鈥檛 comment further, because I felt like I had voiced my opinion and didn鈥檛 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鈥檛 change, it feels like you very much valued our feedback.
Happy new year :)
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鈥檛 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! :)
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鈥攎ore importantly鈥攈uman 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.
@leafpetersen I still see ?.[] in the proposed spec. Is there an issue to update that?
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()
驴Any date or Dart version where this is gonna be implemented, guys?
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;
}
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!
is this supposed to work on dartpad now ?
void main() {
var a = ['1','2','3'];
print(a[0]);
print(a?[4]);
}
@aktxyz No, use nullsafety.dartpad.dev for null safe code until null safety is released (and dartpad updated).
Even at nullsafety.dartpad.dev, that will cause a warning, because a is a List<String>, but not a List<String>?.
Most helpful comment
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:
The
a?[b]:cis 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:
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 treata?[b]:cas a conditional expression and then report an error because that's not a valid map entry.The goal here is to avoid parsers and鈥攎ore importantly鈥攈uman 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:
I think cases where this comes into play are likely to be very rare anyway. A null-aware index operator can evaluate to
nulland how often do you want a map whose key isnull? 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.