Closed stereotype441 closed 4 years ago
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.
@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 ?.[
. 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’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 :)
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! :)
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.
@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>?
.
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 toe1?[e2]
, which would be more intuitive but might be more difficult to parse unambiguously.Which syntax do we want to go with?