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

Allow lazy access to variable in its own initializer. #545

Closed
lrhn opened this issue Aug 26, 2019 · 5 comments
Closed

Allow lazy access to variable in its own initializer. #545

lrhn opened this issue Aug 26, 2019 · 5 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@lrhn
Copy link
Member

lrhn commented Aug 26, 2019

Solution to #542.

Proposal

Allow local variable initializers to refer to the variable inside a nested function expression. (There is no need for a top-level access, that will always be an error).
If the variable is late, use normal late variable semantics.
If the variable is not late, throw on any access happening before the initializer expression has completed.

Rationale

Dart does not allow a local variable initializer to refer to the variable.
It also doesn't allow an instance variable initializer to refer to the variable because the initializer expression does not have access to this.
However, static variables can refer to themselves because they are in the global scope. They are also lazily initialized, and lazy initialization already has semantics for accessing itself during initialization. (Dart 1/2.0 semantics is to throw on cyclic initialization error, NNBD will recurse on the initialization, then throw on the second attempted store if final - it does worry me that we evaluate an initializer expression twice, but you really are asking for it if it happens).

We have defined lazy initialization for late local variables, but have not changed the scoping rules, so it's still an error to be self-referential. If we change the scoping rules, you can write;

late final sub = stream.listen((o) { .... sub.cancel(); ... });

without issues, the semantics are already defined. You then have to read sub to force the initialization to happen.

Also, with late finals, you can already write:

late final StreamSubscription<String> sub;
sub = stream.listen((o) { ... sub.cancel(); ...});

which is annoying, but avoids the "cannot refer to yourself" without actually improving the safety of anything. (Our definite assignment analysis cannot determine whether sub is initialized or not at sub.cancel(), but using late we turn that into a run-time issue).

With that in mind, we can also allow any local variable to refer to itself, with "throw on early access" semantics. It would mean that the variable becomes more expensive, but only for accesses inside the initializer, all other accesses can assume that the variable is initialized.
It is basically equivalent to a rewrite from ... x = ...; into late ... x; x = ...;, which you can do anyway, and definite assignment should recognize that any later access to x is definitely initialized.

So, we would allow something which may fail, but which we already allow by splitting into two lines, and that is what everybody does anyway, so we are not saving anybody from run-time errors, merely making them write more code.

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Aug 26, 2019
@eernstg
Copy link
Member

eernstg commented Aug 26, 2019

You could also argue that the enhancement is unnecessary: It introduces a rather complex semantics, it is most likely only going to be helpful in very few situations, and there is a known work-around. So it's not obvious to me that it passes the "carries its own weight" test.

@lrhn
Copy link
Member Author

lrhn commented Aug 28, 2019

The workaround has so far been to split var foo = ... int into TypeOfFoo foo;foo = ....
That prevents the variable from being final (some people like to do that), means you have to write the type.
It's just plain annoying to write, and a foot-gun for users everywhere. It's a static foot-gun, you get told immediately that you did something bad, but then you have to write the extra code to get around the issue. At least with late-init finals, you can make the variable final, and I do hope we can get type promotion of late-init variables declared using var.

If the work-around is so completely regular, why can't we just make that the semantics of the otherwise invalid code? (Without having to write late).

It is just "very few situations". It actually happens quite often when you write asynchronous code with callbacks.

@rrousselGit
Copy link

Would that work for class properties (assuming it prevents const constructors)?

class Foo {
  late final Future<Foo> foo = _fetchFoo();
}

That's a lot better than:

class Foo {
  Future<Foo> _foo;
  Future<Foo> get foo => _foo ??= _fetchFoo();
}

@lrhn
Copy link
Member Author

lrhn commented Aug 28, 2019

Yes, a late instance variable initializer can access this.

An eager instance variable initializer is executed during object creation, before the object is completely initialized, so it cannot access this.
A late instance variable initializer cannot be evaluated until someone accesses the variable through an object reference, and that means that this has been properly initialized and can be made accessible.

@lrhn
Copy link
Member Author

lrhn commented Aug 19, 2020

The idea was discarded in #828.

@lrhn lrhn closed this as completed Aug 19, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

3 participants