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 https://github.com/dart-lang/sdk/issues/26655
and https://github.com/dart-lang/sdk/issues/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 https://github.com/dart-lang/sdk/issues/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 --version)We would like to see this too. I am one of the commenters on https://github.com/dart-lang/sdk/issues/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.
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.
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.
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?
My selfish enhancement request (and also #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.
@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).
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.
@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.
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.
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 #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.
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.
Very similar to #28950
Any news on this ?
Most helpful comment
We would like to see this too. I am one of the commenters on https://github.com/dart-lang/sdk/issues/28950.
The workarounds that we employ use additional private constructors. For your
class Cexample where you want the public interface to present final values, a workaround could be: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:
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.