Skip to content

constructor initializer list — make expressions able to use earlier names in the list #1394

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

Open
SteveAlexander opened this issue Sep 9, 2018 · 19 comments

Comments

@SteveAlexander
Copy link

SteveAlexander commented Sep 9, 2018

In a constructor's initializer list, I'd like expressions to be able to use variables from the same list that were initialized earlier in the list.

I've read dart-lang/sdk#26655
and dart-lang/sdk#15346

So, I'm glad that it's possible to do this:

class C {
  final int x;
  final int y;
  C(this.x) : y = x + 1;
}

I would also like to be able to do this, which presently gives me an error "Only static members can be accessed in initializers."

class C {
  final int x;
  final int y;
  final int z;
  C(this.x)
    : y = x + 1,
      z = y + 1;
}

I see in dart-lang/sdk#28950 that something similar was requested, and rejected with the rationale "We will definitely not make the initializer list have access to the real object.".

However, I don't want that as such. I'd like to have expressions later down the initializer list able to use initialized earlier in the list.

I meet this from time to time during Flutter development where I want to create a StatelessWidget, and so keep all of its members final, and doing this as part of the initializer for something related to initializing the widget seems to be cleaner than creating a method or getter to do the same.

Also, it feels to me intuitive that it should work this way.

  • Dart SDK Version (dart --version)
    Dart VM version: 2.1.0-dev.4.0.flutter-cd9a42239f (Fri Sep 7 21:08:23 2018 +0000) on "macos_x64"
@rich-j
Copy link

rich-j commented Sep 9, 2018

We would like to see this too. I am one of the commenters on dart-lang/sdk#28950.

The workarounds that we employ use additional private constructors. For your class C example where you want the public interface to present final values, a workaround could be:

class C {
  final int x;
  final int y;
  final int z;
  C._(this.x, this.y) : z = y + 1;
  C(int x) : this._(x, x + 1);
}

This introduces a private constructor that the public constructor calls with computed values. Sometimes it's hard to follow what the computed values are so we may use a factory constructor:

class C2 {
  final int x;
  final int y;
  final int z;
  C2._(this.x, this.y, this.z);
  factory C2(int x) {
    final y = x + 1;
    final z = y + 1;
    return C2._(x, y, z);
  }
}

We have mixed both styles and it can get messy when inheritance is also needed but at least with this private constructor workaround you can maintain the class's public interface.

@lrhn
Copy link
Member

lrhn commented Sep 9, 2018

We already allow access to the value of initializing formals (not the variable itself, it's a new final variable). There is nothing fundamental preventing us from introducing a new variable for initializer list assignments too. It still doesn't allow you to introduce a temporary variable, say creating a pipe and storing its input and output in different variables, you have to store the reused object itself as a field.

There is a trade-off between keeping the initializer list simple, and making it expressive. At the far end of expressiveness, we have Java where you can run any code to initialize the fields. Dart is closer to C++ in design.

@rich-j
Copy link

rich-j commented Sep 9, 2018

Simple is good. Then how about allowing the above class C example to be:

class C {
  final int x;
  final int y = x + 1;      //current error: Only static members can be accessed in initializers.
  final int z = y + 1;      //current error: Only static members can be accessed in initializers.
  C(this.x);
}

This concisely specifies the intent and is what the above workarounds accomplish.

We heavily use this pattern in our reactive architecture where the parameter is a Stream and the referencing expressions are transformations.

@lrhn
Copy link
Member

lrhn commented Sep 10, 2018

Dart initialization is order-independent. You can't see in which order fields are initialized because the fields aren't really initialized until every value is available. (Obviously expressions can have side-effects, so we can see the order of the initializing expressions, but that's a different thing).

If we allow

class C {
  final int x;
  final int y = x + 1;      //current error: Only static members can be accessed in initializers.
  final int z = y + 1;      //current error: Only static members can be accessed in initializers.
  C(this.x);
}

should that then be based on source order or a computed dependency? That is, can I swap the lines around and it still works? That would be nice, so I guess we'd want that. We just compute the dependency graph between initializer expressions at compile-time, and evaluate the expressions in that order. Does that extend to the initializer list too? It probably should, so initializer list expressions are no longer evaluated in source order (unless totally unrelated via dependencies).

And it's still not enough to allow, say:

   Piper() : var tmp = Pipe(), this.in = pipe.sink, this.out = pipe.stream;

That would be neat - temporary local (final?) variables scoped for the (remainder of the) initializer list.
But what if I want to do some computation with loops?

  Clever() : var tmp = ..., var _ = () { for (int i = 0; i < 100; i++) tmp = compute(tmp0; }(), ... tmp ...;

We could allow any statement to occur in an initializer list then, comma-separated. At that point, we might as well do a full initializer block:

  Blocked() : {
     var tmp = ...;
     for (int i  = 0; i < 100; i++) tmp = compute(tmp);
     this.x = tmp.a;
     this.y = tmp.b;
  } {
    actualBody();
  }

The block would not have access to this except to initialize fields. No reading, no calling methods. Any instance variable assigned to by the code is initialized, every other variable is initialized to null.
We can also allow initializer blocks in the class, like Java:

class InitMe {
  final out;
  final in;
  {
    var pipe = Pipe();
    in = pipe.sink;
    out = pipe.stream;
  }
}

That would offer more generality, would definitely not work with const constructors, and would likely require a better "definite assignment" analysis.

I think it's doable, it's just much more complicated than what we have now, so the question is, is it useful so often that it's worth the extra complication?

@rich-j
Copy link

rich-j commented Sep 10, 2018

My selfish enhancement request (and also dart-lang/sdk#28950) is to allow final initializers to access other final initializers. Nothing more complex than that.

We build 90+% of our classes with all final properties - i.e. immutable. There is usually at least one value that isn't know until runtime (buffer, stream, etc.). These are the initialization parameters for the class. Once the initialization (declared as final) values are given to the class, the other final properties are able to be resolved relative to the given final. This is where the current Dart frustrates us - final initializers can't access other final values.

Your Piper class I would like to write as:

class Piper {
  final _pipe = Pipe();
  final in = _pipe.sink;
  final out = _pipe.stream;
}

Your InitMe example is effectively the same as the above Piper class just more verbose. I would prefer the final declarations and relationships to other final fields to be concisely specified. One way to think about final is that they are runtime const with the initialization expression also able to reference other final fields (no circular references).

As to order of the initialization, I suggest following the same logic that Dart uses for const. It appears that these class static constants definitions:

static const y = x + 1;
static const x = 1;

are allowed so it would seem that it's a computed dependency.

BTW - I note that you use var declarations when I see a final value. I am now forced to look at the following code to see if you mutate the value. final values are great that you know that they are only set once when initialized.

The Clever and Blocked examples are too complex for us. If one needs loops and other calculations consider using a factory constructor to get the values and then instantiate with the computed values.

@zoechi
Copy link

zoechi commented Sep 12, 2018

@rich-j
Isn't adding a factory constructor a much easier solution for this?
It's easier to reason about and no added language complexity required that potentially also makes code that doesn't use this feature more difficult to reason about (check if there are any interdependencies between initializers).

@eernstg
Copy link
Member

eernstg commented Sep 12, 2018

Check here for a concrete proposal, along with some arguments why we'd need to address a potentially massive breakage if we were to use it.

@rich-j
Copy link

rich-j commented Sep 12, 2018

@zoechi A functional style with concise property definition, including initialization, becomes a "mindset" (several years of Scala). It'd be great to have class property definitions such as:

final Map<String,dynamic> _jsonMap;
final String name = _jsonMap['name'];
final ID id = ID.fromString(_jsonMap['id']);

Seeing the above in a class would immediately tell me that since _jsonMap doesn't have an initializer it must be provided during construction. name is defined to always be set to the given value from _jsonMap once that value is provided at runtime - it is invariant.

Separating the initialization into constructors from the declaration increases the cognitive load required to understand the code. We do use factory constructors (they aren't inheritable) and multiple levels of named constructors to handle the initializer interdependencies. It just makes for convoluted constructor code.

@lrhn
Copy link
Member

lrhn commented Sep 15, 2018

Something like that above example would likely be written with getters in Dart:

final Map<String,dynamic> _jsonMap;
String get name => _jsonMap['name'];
ID get id => ID.fromString(_jsonMap['id']);

That avoid storing three fields on each object when you only really need one and reduces memory load and churn.

Same problem with the pipe class above - it stores the Pipe object even though nothing uses it.

@rich-j
Copy link

rich-j commented Sep 16, 2018

The class API communicates intent. Declaring a property as final String name vs String get name conveys that the final name is immutable, the getter requires reading the code. Also the above JSON extraction example is simplified, our extraction code does additional data checks and conversions:

name = optionOf(jsonMap["name"]).getOrElse( () => throw new Exception("Person name is required") ),
associatedPersonId = optionOf(jsonMap["associatedPersonId"]).map( (s) => PersonObjId.fromJson(s) ),

where optionOf is from the dartz functional programming library and handles nulls. We want these conversions and checks done during object construction to reject bad data as early as possible and not wait until it's used.

Early in converting our code to Dart we started using getters heavily, but we encountered challenges such as this Dart Angular example:

<...html... *ngFor="let person of (sortedPersons | async)" ...

final Stream<IList<Person>> persons;
Stream<IList<Person>> get sortedPersons => persons.map( (ps)=>ps.sort( Person.orderByName ) );

The Angular async pipe gets mad (:-) since the sortedPersons getter returns a new Stream on each read. Changing implementations over to final variables solved this and several other issues (e.g. debugging) and conveys the fact that over 90%+ of our data is constant once computed (i.e. final).

"reducing memory load and churn" sounds like premature optimization. Space/time tradeoff is always a consideration and is determined by system requirements and design.

The piper class examples that you reference are both hypothetical with one having class body final initializers and the other using a local declaration in an initializer block. Yes, the class body initializer example creates a local variable that is only referenced in other (proposed) initializers and therefore could be considered extraneous. Yes, the local declaration in the initializer block is hidden from the class instance, but are you sure that is doesn't continue to exist in the instance? Based on the discussion in dart-lang/sdk#28950 it appears that instance blocks are created with a scope that is maintained for the life of the object. So, which instance (hypothetically) will use more memory, the extra variable or the extra initialization scope? Both conditions could be optimized away with the compiler. In day-to-day development, until we can measure the impact, our system requirement is to write clear, concise, straightforward and maintainable code.

@lrhn
Copy link
Member

lrhn commented Sep 17, 2018

The class API communicates intent. Declaring a property as final String name vs String get name conveys that the final name is immutable, the getter requires reading the code.

I'd have to disagree on that particular reading of code. A getter without a setter means exactly the same thing, and you would have to check for the absence of a setter anyway, even if you declare the getter using a final field.

Apart from that, then obviously different use-cases need different approaches, and your coding style seems to be doing a lot of computation up-front, which means caching it is reasonable, likewise anything depending on identity should be coded to respect that.

@lrhn
Copy link
Member

lrhn commented Dec 6, 2018

Very similar to dart-lang/sdk#28950

@dart-lang dart-lang deleted a comment Oct 14, 2019
@Nico04
Copy link

Nico04 commented Jun 4, 2020

Any news on this ?

@aureldussauge
Copy link

aureldussauge commented Dec 23, 2020

I try to initialize a Subject property, and a Stream property with some RX operators in the constructor but I struggle because I can't reuse my Subject variable in initializers. Factories are problematic too because I want inheritance.

It's something that I was doing in Typescript, and I thought it was very basic.

I removed the final keyword on the properties, and initialized them in the body of the constructor, but another problem occurs when using the null-safety. Every property which is initialized in the body of the constructor instead of in the initializers list must be nullable. I don't want every RX observable that I create to be nullable.

@lrhn lrhn transferred this issue from dart-lang/sdk Jan 5, 2021
@larssn
Copy link

larssn commented Mar 18, 2021

@rich-j Have you considered late final now that we have null safety?

Dartpad example:

class LateTest {
  LateTest() : stream1 = Stream.periodic(Duration(seconds: 1), (val) => val).map((val) => val).asBroadcastStream() {    
    stream2 = stream1.map((val) => val.toString() + ' cookie' + (val > 1 ? 's' : ''));
    stream3 = stream1.map((val) => val.toString() + ' l milk');
  }
  
  final Stream<int> stream1;
  late final Stream<String> stream2;
  late final Stream<String> stream3;
  
  void start() {
    stream2.listen(print);
    stream3.listen(print);
  }
}

main() {
  LateTest()..start();
}

I realize this introduces a runtime check for each late final variable, and I'm not sure if it has any performance implications. Maybe someone can elaborate on that.

@rich-j
Copy link

rich-j commented Mar 18, 2021

@larssn Yes, we are happily mostly using late final now instead of constructor initializers. late final allows the definition to be provided with the declaration. To expand on your example...

class LateTest {
  
  final int count;
  final Duration waitTime;
  
  LateTest({required this.count, this.waitTime = const Duration(seconds: 1)});
  
  late final Stream<int> stream1 = Stream.periodic(waitTime, (val) => val).take(count).asBroadcastStream();
  late final Stream<String> stream2 = stream1.map((val) => "$val cookie${val > 1 ? 's' : ''}");
  late final Stream<String> stream3 = stream1.map((val) => "$val l milk");
  
  void start() {
    stream2.listen(print);
    stream3.listen(print);
  }
}

main() {
  LateTest(count: 3)..start();
}

We mostly code using immutable objects that depend on provided external values (e.g. from network or database) that other values are derived from.

As for performance it's always a tradeoff. Yes, late final will have to check every access but in most code paths that should be a negligible (i.e. don't prematurely optimize). In the above example using declaration initializers, if you only access stream2 the initialization of stream3 wouldn't need to happen.

@sebastianhaberey
Copy link

+1

From a new Dart user's perspective, this definitely seems like a kink in the language.

According to the documentation, the late keyword is intended for variables that are not initialized at the time of their declaration and variables that need lazy initialization. Both of these criteria don't fit well; what we'd really like to have is initialization at the time of declaration for variables that depend on each other.

For me it comes down to a question of wording: "late" just doesn't fit for what I'm trying to achieve. It just feels wrong.

@AlexanderFarkas
Copy link

AlexanderFarkas commented Jul 3, 2024

@sebastianhaberey
I use it this way:

late final field = this.instanceField + 2;

The whole State Management library I have created relies on this feature.

@lrhn
Copy link
Member

lrhn commented Jul 3, 2024

Worth noticing that the suggestions to allow fields to access other fields,

class C {
  final int x = 1;
  final int y = x + 1;
}

are less viable with the introduction of the augmentation feature currently being design.

The augmentation feature allows an augmenting declaration to override/augment the getter and setter of a field declaration, which means that x in final int y = x + 1; doesn't refer to a variable, but to a getter that can do anything, and which cannot be allowed to run before the object is completely initialized.

That was actually already the case before. If C was followed by:

class D {
  int otherX = 42;
  int get x => someRandom.nextBool() ? super.x : otherX;
}

it's not clear that final int y = x + 1; should refer to the value of the x field initializer, and not to the instance getter. It would have to be defined that way, and then

 final int y = x + 1;
 late final int z = x + 1;

would behave differently for the exact same code. That's not great.

With augmentations, it can be done inside the same class:

class C {
  final someRandom = Random();
  int otherX = 42;
  final int x = 1;
  final int y = x + 1;
  augment get x => someRandom.nextBool ? augmented : otherX;
}

The reference to x in final int y = x + 1; should refer be a this.x member access, which means it hits the getter and cannot be allowed in an initializer.

(I also want to refer to prior values in initializer lists. As late as today I did a workaround with a second constructor, just to be able to initialize what this feature would allow you to write as just

 : currentTime = (someExpression).millisecondsSinceEpoch, nextTime = currentTime - 1;

So there is demand!)

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

No branches or pull requests

11 participants