Skip to content

lazy keyword: it is a final that is bound to the constructor #1236

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

Closed
jodinathan opened this issue Sep 24, 2020 · 23 comments
Closed

lazy keyword: it is a final that is bound to the constructor #1236

jodinathan opened this issue Sep 24, 2020 · 23 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@jodinathan
Copy link

Hi,

I have published a package yesterday: https://pub.dev/packages/dispose

It basically exposes a class to bind StreamSubscriptions, Timers and StreamControllers. So those listeners are cancelled/closed in the dispose() function.
I was tired of having to bind variables to cancel/close stuff.

For example, instead of final _ctrl = StreamController() in a class, you have to:

class Foo extends Disposable {
  StreamController _ctrl;
  Stream someInfiniteStream;
  
  Foo() {
    _ctrl = controller(); // this will be closed in dispose()
   each(someInfiniteStream, (item) => null); // the listener will be cancelled in the dispose()
  }
}

I read the page https://dart.dev/null-safety and started playing with NNBD.
As I have ADHD, I just searched for the late keyword and couldn't find if could be used along with final.

Well, as I try to improve the Dart community, I've created an SO question:
https://stackoverflow.com/questions/64038304/can-late-and-final-be-used-together

Weirdly I was downvoted, but oh well.
Someone linked the explanation regarding late final and even thought it is a good thing, I think we can have a benefit for more strict late.

Hence the lazy keyword, it sits between late and final.
Basically a final variable that must be set in the constructor and can not be changed.

What is so different from late final?
It is checked at compile time and there can not have any setters to it.

@jodinathan jodinathan added the feature Proposed language feature that solves one or more problems label Sep 24, 2020
@lrhn
Copy link
Member

lrhn commented Sep 24, 2020

Dart currently distinguishes two phases of initialization of a new object instance: Before constructor bodies and after that.
Until constructor bodies start running, there is no access to the new object, all you can do is to say how you want the object's fields to be initialized. That's what the initializer list, initializing formals and fields with initializers are for.

After that phase, it's possible to refer to the object through a reference, and we give up all attempts to figure out whether that happens, so at that point all non-late final variables must have been given a value. Dart does not allow you to see a final fields in an uninitialized state.

The lazy you propose sounds like it would work just like late except that there wouldn't be a setter at all, and instead only the constructor body is allowed to store to the field (which would then not be using a setter, but directly store into the field, the same way the initializer list is doing).

That's what the initializer list is for. Your code can be written;

class Foo extends Disposable {
  final StreamController _ctrl;
  Stream someInfiniteStream;
  
  Foo() : _ctrl = controller(); { // This will be closed in dispose()
    each(someInfiniteStream, (item) => null); // the listener will be cancelled in the dispose()
  }
}

So, no lazy needed if the initializer list can be used instead.

@leafpetersen
Copy link
Member

So, no lazy needed if the initializer list can be used instead.

I do think it's interesting to think about what we can do for the cases where the initializer list can't be used. This definitely comes up. I've encountered this same issue: I need to use late in order to initialize it in the constructor, but the fact that it induces an externally visible setter is just a trap for users. I think @kevmoo and @natebosch might have raised this in a migration code review early on as well?

@jodinathan
Copy link
Author

That's what the initializer list is for. Your code can be written;

class Foo extends Disposable {
  final StreamController _ctrl;
  Stream someInfiniteStream;
  
  Foo() : _ctrl = controller(); { // This will be closed in dispose()
    each(someInfiniteStream, (item) => null); // the listener will be cancelled in the dispose()
  }
}

So, no lazy needed if the initializer list can be used instead.

controller() is a method of the Disposable class, so it can't be used in initializer list. Maybe I am doing something wrong.
NullSafety Dartpad said I couldn't use method() in initializer list in the code below:

class Foo {
  int method() => 5;
  final int bar;
  
  Foo() : bar = method() {}
}

void main() {
  
}

@kevmoo
Copy link
Member

kevmoo commented Sep 24, 2020

I think @kevmoo and @natebosch might have raised this in a migration code review early on as well?

Indeed. Something to flag for public late fields.

@natebosch
Copy link
Member

A public late final field is a recipe for trouble IMO. Might even be worth a hint or a lint.

Regarding lazy, my gut reaction is that I would prefer to extend the capabilities of initializer lists, such as with #622, instead of trying to blur the line between constructor body and initializer.

@jodinathan
Copy link
Author

jodinathan commented Sep 24, 2020

A public late final field is a recipe for trouble IMO. Might even be worth a hint or a lint.

Regarding lazy, my gut reaction is that I would prefer to extend the capabilities of initializer lists, such as with #622, instead of trying to blur the line between constructor body and initializer.

I am not sure if that issue would solve this and also not sure if extending the initializer lists capabilities to be able to access the class methods could harm optimizations

@lrhn
Copy link
Member

lrhn commented Sep 25, 2020

The proposal here is to have a way to set the field without a setter, one which is only usable inside the constructor body (and maybe/maybe not inside nested functions). It's not a setter because it's not part of the interface. A private setter could have the same effect, and would allow other code in the same library to cooperate about the initialization.

The obvious solution is the classical "private field, public getter" combination.

Maybe if there was a way to give implicit getters and setters different names (say, one private and the other public).
That could be useful in many other cases too, including the "private field, public getter" pattern.

Strawmanning:

int x: { get; _set; } // meaning a field with an implicit public getter, `x`, and an implicit private setter `_x=`.

Maybe we should just allow naming them explicitly:

int {get x, set _x} = 24;
int {get foo, set bar}; // if you *really* want it.
late final int {get y, set _y; };  // works for any variable declaration.

So, where the name would otherwise occur in a (non-local) variable declaration, you can write a name declaration block: {get name, set name2}, and a single identifier x is just equivalent to {get x, set x} (unless final non-late, then it's just {get x}).

@eernstg
Copy link
Member

eernstg commented Sep 25, 2020

It looks like the original example could be expressed as follows:

class Foo extends Disposable {
  late final StreamController _ctrl = controller(); // This will be closed in dispose().
  late Stream someInfiniteStream; // Some initialization assumed, not shown.

  Foo() {
    each(someInfiniteStream, (item) => null); // The listener will be cancelled in dispose().
  }
}

The difference between using laziness (late final with initializer, where access to this is allowed) and allowing raw storage operations in the constructor (and having no setter) include performance (it may be more expensive to evaluate the late variable) and ordering (the constructor can mandate a specific ordering in the initialization of several native-write-no-setter fields). On the other hand, the guarantees offered by the language that "we won't see an uninitialized field" will be very tricky to maintain.

With lazy fields the ordering is implicit, more declarative, but may enter an infinite loop; that may be considered an improvement, at least as long as it is so simple that termination is obvious (whatever that means, YMMV ;-).

@jodinathan
Copy link
Author

For me, a language is just how I express myself to a compiler, be it JS, ByteCode, Assembly etc.

"Dear compiler, this property, independently if private or public, is final.
Unfortunately, I can't set it in the initiliazer process because I need the instance to be alive at the moment of its setting.
However, if I assure you I will only set it in the constructor, can you treat it as a final and make any optimizations you would with a de-facto final property?
Thanks and nice job btw"

@lrhn
Copy link
Member

lrhn commented Sep 25, 2020

"Dear Jodinathan.
I can't.
— the compiler."

The problem here is that if the field is not set when the constructor body starts running, then most optimizations that you can do with final fields are off the table. Being inside the constructor body is not magical in any way, you're already past the magic window in time where the object is safe from snooping and manipulation.

(That said, there aren't that many optimizations of final instance fields anyway, because a subclass might override the field with a getter. You need a whole-program analysis to make sure that doesn't happen.)

@jodinathan
Copy link
Author

ok. So even with no optimizations at all, it still has no setter which makes code less prone to error.

@escamoteur
Copy link

I too often wish I could initialize final variables in the constructor. ´late´ has the problem that it only works with nonnullables. But what if the initialization depends on some optional parameter?
What I would be looking for is a ´write once´ property type that can get a value assigned exactly one time inside the constructor that is otherwise null.

@leafpetersen
Copy link
Member

I too often wish I could initialize final variables in the constructor. ´late´ has the problem that it only works with nonnullables.

This is not true, you can have a nullable late field.

What I would be looking for is a ´write once´ property type that can get a value assigned exactly one time inside the constructor that is otherwise null.

late final is almost what you want, except you have to manually assign null if you don't initialize it.

@escamoteur
Copy link

@leafpetersen Ah, that is interesting, I thought it wouldn't make sense to have late with nullable fields.
Good to know.
What happens if I don't a value to a late nullable field?

@eernstg
Copy link
Member

eernstg commented Nov 10, 2020

With a late or late final variable without an initializing expression you get a dynamic error if and when it is evaluated and no value has been assigned to it (and it makes no difference whether the type is nullable).

@escamoteur
Copy link

would it be possible for the compiler or linter to check if any ´final late´ aren't initialized at the end of the constructor?

@eernstg
Copy link
Member

eernstg commented Nov 11, 2020

It is probably not a good idea to enforce that every final late instance variable must be initialized at the end of each constructor: There could be cases where the initialization is intended to take place later.

Also, late is associated with dynamic checks, and the static analysis isn't optimized for telling whether the initialization did actually occur, it is just checking that it could have occurred.

But you could add a list literal evaluating each of your late final variables (like [a, b, c];) and that statement would throw if one or more of them hasn't been initialized, or you could use an assert (assert([a, b, c] != null);).

@escamoteur
Copy link

@eernstg Du you think it would be possible to implement a linter for that?

@eernstg
Copy link
Member

eernstg commented Nov 12, 2020

It is certainly possible to implement a lint that checks at each return statement and at the end of the constructor (if it is reachable) that every late final instance variable of the enclosing class isn't definitely unassigned, or that it is definitely assigned. It may not be so easy to implement, however, because this kind of analysis is currently only performed for local variables.

@TheKashe
Copy link

The lazy you propose sounds like it would work just like late except that there wouldn't be a setter at all, and instead only the constructor body is allowed to store to the field (which would then not be using a setter, but directly store into the field, the same way the initializer list is doing).

That's what the initializer list is for.
So, no lazy needed if the initializer list can be used instead.

That is not always the case!

1. Mixins can not have constructors
Therefore initializers can't be used with mixins

2. Instance members can't be used in initializers

class Foo{
  computeBar(someParam){...}
  final wrapBar=>wrapFunc(computeBar);  //Not supported
}

There is a very problematic consequence: it's currently not possible to implement immutable Mixins with memoisation. Eg, the above Foo, needs to be

class Foo{
  computeBar(someParam){
    if(wrapBar==null)
      wrapBar=>wrapFunc(computeBar); 
  }
  var wrapBar;
}

"layz wrapBar" would be a way to get past initializer limitations and currently there is no adequate alternative!

@eernstg
Copy link
Member

eernstg commented Jan 5, 2021

@jodinathan, the original issue is handled by using a late final instance variable with an initializer, as mentioned here. The solution looks as follows, and the main point is that the initializing expression in the declaration of _ctrl does have access to this:

class Foo extends Disposable {
  late final StreamController _ctrl = controller(); // This will be closed in dispose().
  late Stream someInfiniteStream; // Some initialization assumed, not shown.

  Foo() {
    each(someInfiniteStream, (item) => null); // The listener will be cancelled in dispose().
  }
}

I think it would make sense to close this issue. Please create a new issue if the lazy modifier and a variant of the ideas presented along with it could handle some other situation/example which is currently not well supported in Dart.

@TheKashe, maybe you could create a new issue with a more complete version of your example?

Currently, it is not obvious (to me, at least) how to interpret the code that you mention.

class Foo {
  computeBar(someParam) {...}
  final wrapBar => wrapFunc(computeBar);
}

is a syntax error. One way to repair it would be

class Foo {
  computeBar(someParam) {...}
  T get wrapBar => wrapFunc(computeBar);
}

where the return type T depends on the declaration of wrapFunc (that we don't know). The word memoization might hint in the direction of the following:

class Foo {
  computeBar(someParam) {...}
  late final wrapBar = wrapFunc(computeBar);
}

which is the solution that I mentioned earlier. In any case, it would be helpful to get some more details about the situation that you're focusing on, and about any proposals for how to handle it.

@eernstg eernstg closed this as completed Jan 5, 2021
@jodinathan
Copy link
Author

I think it would make sense to close this issue. Please create a new issue if the lazy modifier and a variant of the ideas presented along with it could handle some other situation/example which is currently not well supported in Dart.

@eernstg we were just finishing a project in TS and I remembered this topic.
This is exactly how it happens in TypeScript: a property must be initialized in the class declaration or in the class constructor to be a valid non-nullable property.

@eernstg
Copy link
Member

eernstg commented Oct 4, 2021

There have been many discussions about restricting the possible actions taken by code in a constructor, e.g., that there should be a marker which could be placed somewhere in the list of statements in a constructor, and it would be a compile-time error to do certain things before that marker.

If we had a mechanism like that then it could make sense to consider a phrase like

must be initialized ... in the class constructor

as a useful language rule. However, there are many reasons why Dart does not have a mechanism like that (I can't immediately find an issue specifically on this topic, but I know it has been debated again and again), and this means that there is no difference between code in a constructor and code anywhere else. For instance, you could call main from anywhere in a Dart constructor, which means that any step in the execution of a constructor can do anything that a Dart program can do.

So it doesn't mean anything to say that X can only be done in a class constructor, for any X, because you don't have any guarantees that anything specific can't happen.

The Dart approach to construction is to have actual guarantees, like, "no final variable can be observed to have two or more distinct values", as opposed to Java), or "during construction of a fresh object, until the end of the phase where the constructor initializer lists have been executed, this cannot be accessed in any other way than the initialization of fields that is performed by initializer list elements".

Hence, as soon as the first constructor body starts executing, we're in normal execution land, and there are no restrictions on what you can do. In particular, it doesn't make sense to say "you can only do X in the body of a constructor".

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

8 participants