Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can late alone be used to declare a variable? #321

Closed
leafpetersen opened this issue Apr 18, 2019 · 10 comments
Closed

Can late alone be used to declare a variable? #321

leafpetersen opened this issue Apr 18, 2019 · 10 comments
Labels
nnbd NNBD related issues

Comments

@leafpetersen
Copy link
Member

We propose to add a keyword late for marking late initialized variables. This means that for the following declarations:

int x;
final int x;

there are corresponding late variable declarations as follows:

late int x;
late final int x;

What is the late declaration that corresponds to:

var x;

Is it

late var x;

or

late x;

The former is consistent with the treatment of covariant, the latter with the treatment of final.

Thoughts?

cc @munificent @lrhn @eernstg @danrubel @stereotype441 @bwilkerson

@lrhn
Copy link
Member

lrhn commented Apr 18, 2019

I'm split here. My personal preference is to go with late x; over late var x; because it should act like final. That's the kind of modifier it is.
I do fear that that might be confusing to some people who have learned that var is what you use to introduce a variable. It's the same as final, so maybe it's not an issue.

It's likely a moot point without an initializer expression because a late x;/late var x; would have type dynamic and be nullable, and not need to be late at all.

A more interesting case is late x = foo(); vs late var x = foo(); where foo introduces a type.
Writing late var x = foo(); still feels like overkill to me.

@bwilkerson
Copy link
Member

I agree. Given that late is more similar to final than it is to covariant, it seems like it ought to be consistent with final. And it's a nice bonus that it happens to be more concise.

@munificent
Copy link
Member

I don't know if I have a coherent take on this, but here's a brain dump:

Allowing it would make it sort of consistent with final. But I think the way final works is quite confusing. It's a modifier, but also it's a declarator.

This dual nature was one of the sticking points with option semicolons:

final veryLongIdentifier
    anotherLongIdentifier // <-- Part of previous line or not?

We get a lot of feedback from users that they don't like final as a declarator. Much of that is that it's long, but I also think it's partially because it doesn't read like a declarator to them like let or val does. I think the late keyword is even less likely to read like a declarator to someone because there's nothing "variable-like" about that word and it's not used in any other language that I know of at all, much less to declare a variable.

In something simple like:

late x;

Sure, maybe it's pretty obviously some kind of variable declaration thing because there's only a name after it. But if we add destructuring, it gets weird:

late [x, y] = someFunction();

It's hard for me to look at that and not find it very opaque. (I don't know if we will support late destructured variables, but I'm assuming here that we might.)


Note that allowing late x still doesn't make it totally consistent with final. The two aren't parallel constructs because they can be composed. And when they're composed, only a certain proscribed order is allowed.

Also, you can use final for parameters, but not late.


I am also worried that inferring late x to mean non-final encourages the wrong practice. Most other languages are leaning heavily towards immutability. We can't (easily) undo the damage we already did with final being longer than var and typed variables defaulting to immutable, but I'm not crazy about getting even farther in the hole:

late x;
late final x; // 6 characters longer. :(

The way I think about features like this that introduce pretty novel (relative to the larger software world) behavior is that we should err on the side of explicitness. Users pick up syntax much quicker than semantics (ex: initializing formals versus library privacy). So if we have some new, interesting behavior, I want the syntax to be as simple as possible and telegraph it clearly.

I think it's simple and clear to say "'late' is a modifier you can put before a variable declaration".


One way to think about the cognitive load of this is to look at the grammar. Right now (simplified, ignore multi-variable declarations), the grammar is something like:

variable = declarator name ( "=" initializer )? ";" ;

declarator = "var"
           | "final"
           | "final"? type ;

If we treat late as purely a modifier, it becomes:

variable = "late"? declarator name ( "=" initializer )? ";" ;

declarator = "var"
           | "final"
           | "final"? type ;

If we allow it as a declarator or a modifier, I think it's:

variable = declarator name ( "=" initializer )? ";" ;

declarator = "var"
           | "late"? "final"
           | "late"
           | "late"? "final"? type ;

That feels hairier to me once you expand out all the ? to think about which combinations actually are and aren't allowed. I'm assuming here that we explicitly disallow late var x.


We don't treat static or covariant like final. You can't do:

class Foo {
  static x = 123;
  bar(covariant y) {}
}

We also don't treat required like final. Of course, you can do this:

foo({required x});

But you can also do:

foo({required var x});

(I don't know why we allow var for parameters, but we do.)

So I don't know if it's very compelling to say that late should behave like final. We already have other keywords that come before variables that don't work like final and they seem fine.


I don't know if any of this is compelling, but I can't shake the feeling that treating late like a declarator and not simply a modifier is the wrong choice.

@leafpetersen
Copy link
Member Author

If we allow it as a declarator or a modifier, I think it's:

variable = declarator name ( "=" initializer )? ";" ;

declarator = "var"
           | "late"? "final"
           | "late"
           | "late"? "final"? type ;

This isn't actually how the grammar is (among other things, it ignores const), but even so, this is more complicated than it needs to be:

variable = declarator name ( "=" initializer )? ";" ;

 declarator = 
              "var"
            | type
            | "final" type?
            | "late" "final"? type? ;

vs

variable = late? declarator name ( "=" initializer )? ";" ;

 declarator = 
              "var"
            | type
            | "final" type?

I think the former is about as clear as the latter.

@bwilkerson
Copy link
Member

bwilkerson commented Apr 26, 2019 via email

@lrhn
Copy link
Member

lrhn commented Apr 29, 2019

We only allow one order, and the order is generally from more impactful to less.
The more something affects the API, the earlier it is (except the name should probably be before the type with that argument).

So static is always first because it completely changes how the member is accessed. In the mental ordering, we split statics from instance members before thinking about what those members are.
A final variable only introduces a getter, a non-final introduces a geter and setter, so it affects the presence of a member. Then the type just restricts the allowed values.

For late, it is really at the same level as final, and it depends on the intended behavior whether it's more or less specific.

If you do late final int x; (a write-once integer) and allows users to initialize the variable, then late is probably more important than final.
If you do final late buffer = StringBuffer(); (write-zero, lazy initialization) and just use late for lazy initialization, then final is more important, and late is an implementation detal (at the level of an async marker).
If you have the "slightly late initi" like:

final late String name;
final late int age;
if (something) {
   name = ...
   age = ...
} else {
   name = ...
   age = ...
}

then the late-ness is very localized, and the remainder of the life-time of the varible should just treat it as final.

That's why I think there isn't one answer which fits all use-cases. I expect the publicly late write-once case to be rare compared to the lazy initialization, and I prefer final late to late final for that.

@febs
Copy link

febs commented May 24, 2019

Just wondering, why to use 'late' rather than 'lateinit'? Most developers are comfortable with that already.
Thank you!

@munificent
Copy link
Member

munificent commented May 28, 2019

A few reasons:

  • late is an existing word whose meaning people know, lateinit is a term coined by Kotlin and only meaningful to the subset of programmers who've used that language.

  • late is half the length.

  • late in Dart is not just for initialization. It doesn't have the same semantics as lateinit.

@jodinathan
Copy link

maybbe late for non-final and latef or late: for final late?
late final int x; is really long.

I would rather type something like :int x = 0 or int x := 0 for final and late int x: for a late final.

@leafpetersen
Copy link
Member Author

Ok, per discussion:

  • late is a modifier
  • late comes before final, var or <type>

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

No branches or pull requests

6 participants