-
Notifications
You must be signed in to change notification settings - Fork 211
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
Stable getters #1518
Comments
I don't think this actually solves most of the problems people will have with promoting instance variables. Also, if you know the variable is stable, by declaration or just by promise, then it's always safe to cache it in a local variable, without worrying about concurrent modifications. In that case, any feature which introduces a local variable copy of the value is just as safe, and it can work with mutable variables too. (Also, can I haz stable getters? 😁 ) |
😁 It's a trade-off: Shadowing variables will allow the developer to cache a non-local getter result relatively easily and safely. That's possible even in the case where the non-local getter is not stable (it could still be correct according to the assumptions of the software to cache its value and keep using it for a while), and it is a documented choice, in the sense that the But declaring that a getter is stable establishes that fact as a design choice at the declaration. This gives us a semantic guarantee which can be conceptualized (it's like "this property of the object is immutable"), and that influences all the code which is written to use that getter. On top of this conceptual benefit, we get the ability to (auto)shadow the getter and promote it. This makes stable getters a rather principled device that also yields practical benefits, whereas shadowing is motivated by practical benefits, with foundations that are more shaky (we are reusing a cached value for a while, and that may or may not be correct in the given context). |
For a getter to be declared stable, it must return the same result every time, and in particular I think it's likely to be a massively breaking change to use However, when an immutable variable can be declared with We'd basically migrate by performing a global search-and-replace from |
(1) Implement stable getters. (2) Hack the result to artificially add I think no language has the notion of stable getters. The crucial property is that it returns the same result every time, but this is usually achieved by making it the foundation of the language (pure functional programming), or by preventing overrides (e.g., Java and C++ variables do not have a notion of overriding, getters and setters must be written manually). The interesting bit here is that a stable getter can be a lot more expressive than just a fixed memory cell read. |
We will never run out of PhD projects that someone should do. 😸 |
The downside of this is you need to write From a high level, it still seems most correct to me, to make the edge case the edge case.
|
That's kinda tricky. Consider the following: int? get maybeNull => math.Random().nextBool() ? 1 : null; There's no explicit method call, but this is exactly the situation Dart is preventing by refusing to promote fields. On a broader note, I keep getting confused between this issue and #1514. Are we trying to provide a solution for promoting fields, or defining a new (maybe default) behavior for getters? If the latter is just a means to an end, I'd say that IMO, it's not worth the trouble of just adding a local variable to cache the value for promotion, and being a little careful when using |
I don't see the issue if we just said that getters and methods can not be promoted. The glaring issue here is a developer declaring a field, and dart telling them it can't check their field for null. It just makes no sense from an authoring perspective. Conversely, it makes sense, on its face, why you could not trust a getter to provide the same result each time. I know under the hood, dart doesn't make much of a distinction here, but developers use these semantics with clear intent. Unless maybeNull is overriding a base class? Then it should be forced to be this, so everything is declared in the open:
|
@Levi-Lesches wrote:
The motivation for #1514 and |
@esDotDev wrote:
The core issue here is the semantics of instance variables. You can find many, many examples where software people express the rule that instance variables should be private. For instance, https://stackoverflow.com/a/14399978/5603708 says (in a question which is tagged OOP and C++, but referring to Java as well):
The rationale is typically that it creates too many constraints on the implementation of a given interface if it has been cast in concrete that a specific member is a storage location in the object. So we should write methods (getters and setters) and let clients call them, they shouldn't access the instance variable directly. Dart was designed to eliminate the need to write all those getters and setters by making them mandatory. It's often possible for a compiler to eliminate the getter/setter invocations, but you still have the encapsulation because you won't have to edit your declarations and all the call sites (that may occur in code that you don't control) in order to introduce getters and setters, you just have them by default. So if we can't promote getters then we can't promote instance variables! In contrast, this proposal is all about enabling promotion of getters (hence instance variables), without reducing their ability to support overriding and arbitrary computations. I believe the concept of a stable getter is useful from a software engineering perspective: you're declaring immutability, not a specific implementation/representation. |
@eernstg So basically we're looking for a shortcut for the following: class Temp {
int? get maybeNull => 3;
int roundNumber() {
final int? value = maybeNull; // <-- stable getters would make this unnecessary
if (value == null) return 0;
else return value.round(); // <-- can be promoted now
}
} That's why I made the comparison to shadowing -- it seems that just adding this one line fixes both problems. |
@Levi-Lesches wrote:
class Temp {
int? get maybeNull => 3;
int roundNumber() {
final int? value = maybeNull; // <-- stable getters would make this unnecessary
if (value == null) return 0;
else return value.round(); // <-- can be promoted now
}
} No, stable getters are actually quite different.
Other forms like When a getter is marked stable, however, it must satisfy the constraint: If we call it twice on the same receiver it must return the same value (and the compiler must be able to verify that fact statically). I don't see any indication in the example above that On the other hand, the example maps directly to a shadowing variable: class Temp {
int? get maybeNull => 3;
int roundNumber() {
shadow maybeNull;
return maybeNull == null ? 0 : maybeNull.round();
}
} |
@tatumizer wrote:
That is true, but the motivation for stable getters is the conceptual content: I would like to be able to declare explicitly that a particular getter has the stated immutability property, and then I'd like to allow for all the expressive power we can have while preserving that property. So I'm not going for the best possible fit for instance variable promotion, I'm going for a useful language concept. With that, I'm noting that stable getters are highly relevant to instance variable promotion because (1) any stable expression can be promoted (and that's a lot more than just a single getter invocation), and (2) any stable expression can be cached (implicitly, by the compiler). We just need to write Another thing is that class C {
int? x;
C(this.x);
void Function(int) get use {
x = null;
return (int i) {};
}
void foo() {
if (x != null) {
use(x!); // `x` _is_ actually null, so we can't omit `!`.
}
}
}
void main() => C(2).foo(); |
After reading the proposal more closely it is quite nice, and does knock off the majority of problem cases as they relate to flutter. All widgets params are final, for example, so right off the hop you are probably addressing 80% of the headaches with this. I see how this is adding to the language with a useful feature, whereas But, if all stable fields must be final, then something like this wouldn't work?
Also, if |
This is the perfect description of the confusion devs are encountering. No one will call the compiler "stupid" in the latter, I'm seeing constant complaining about the former because it's just non-obvious and no one gets it. I receive a whole lot of these when I try and explain how it works: 😕 |
@esDotDev wrote:
int? counter;
void increment(){
if (counter == null) return;
counter++;
} True, that wouldn't work: We can't trust int? counter;
void increment(){
if (shadow: counter != null) counter++;
}
In some cases a compiler/analyzer is just forced to stop tracking down partial solutions to problems that are undecidable in general, so in some cases the compiler/analyzer will deny a truth that developers may find obvious. However, in cases where we actually have unsoundness it's just so much more important that the compiler/analyzer can detect that a given assumption can be false. This will be true in some of those situations where the unsoundness isn't obvious... |
Cool, just wanted to make sure I was understanding properly. I don't think it would be a major issue, at least it's a lot better than what we have now :)
Totally understand, its just this specific case is so non-obvious will continue to jump out at developers until it is addressed. |
@tatumizer wrote:
Agreed, 'final' is rather definitive. However, 'final' has had a specific meaning in Dart for many years, and we still need that semantics (say, in a declaration of a top-level variable which doesn't admit assignments). So we do need a new syntax, and
final late int _x1;
final late int _x2;
final late bool cond;
stable get x => cond? _x1 : _x2; The final variables must be library or instance variables, because otherwise we couldn't have a getter at the same level. First, if they are instance members then we can't make If they are top-level declarations then we could in principle allow |
I was also about to bring up class Temp {
late final int? tbd;
/// The compiler knows by this point whether the value has been set or is still null.
/// Since it's final, the value won't change -- whatever it is now it will be through the rest of the function.
int roundValue() {
if (tbd == null) return 0;
else return tbd.round();
}
} It seems like we're looking for a It doesn't quite make sense to me to allow users to provide their own |
@Levi-Lesches wrote:
Yes. A final instance variable has an implicitly induced getter which can be stable (so it's OK to override a stable getter with a final instance variable and it is OK to mark a final instance variable as stable, in which case (Various other rules differ in those cases, but all those kinds of variables do satisfy the requirement that their getter will return the same value each time, for the same receiver).
It's not so hard to specify a range of different implementations which are allowed: The notion of a stable expression already allows a lot of different implementations, and we could easily add a few extra cases (for instance, if Also, it would surely be inconvenient if we couldn't use a constant, e.g.,
This comment is misleading: The value of a late variable with no initializer and a nullable type isn't initially null, the variable actually doesn't have a value. So the following throws, rather than printing null: var b = false;
void main() {
late final int? i;
if (b) i = 10;
print(i); // Throws!
} |
Right, I mentioned Out of curiousity, is there a natural way to use the class Temp {
final int var1 = 1; // stable (except for subclasses)
int get var2 => 2; // not stable, but should be
final int get var3 => 3; // this feels natural IMO
stable int var4 => 4; // will always be stable, even if overriden
} |
As an instance variable, and in the same class or mixin? That actually depends on the form: A non-local class C {
late final int i = 14;
set i(_) {} // OK.
late final int j;
set j(_) {} // Error.
} (By the way, that error wasn't implemented consistently, cf. dart-lang/sdk#45398, but it is an error).
If there is an expression class C {
// Assume that this won't work:
stable get g => e; // Error "`e` is not stable".
// Then you can do this:
stable late final g = e; // OK, will evaluate `e` once, when first used.
} You don't have to include
We'd use
It's the other way around: A I suspect that a large proportion of all public final instance variables could be stable, and program correctness would benefit from doing that. We could basically do a search-and-replace on instance variables and change |
That's a really good point. So the problems of const should never apply here, which is nice. Back to my point on class Temp {
late int value = Random().nextInt(5);
}
class Temp2 extends Temp {
@override
int get value => Random().nextInt(5);
}
void main() {
Temp temp = Temp();
print(temp.value); // 4 (guaranteed to be statistically random -- https://xkcd.com/221/)
print(temp.value); // 4
print(temp.value); // 4
Temp2 temp2 = Temp2();
print(temp2.value); // 1
print(temp2.value); // 2
print(temp2.value); // 3
} So
But the problem with null-safe promotion of fields is often with mutable fields, it's just that the value is guaranteed not to change in the getter itself. If class Person {
/// Can't fix this with `late`, since it can be set to null later.
/// But on the other hand, this getter never changes the value.
String? favoriteColor;
String introduction() {
if (favoriteColor == null) {
return "I don't have a favorite color yet/anymore";
} else {
return favoriteColor.toUpperCase(); // Can't promote!
}
}
}
void main() {
final Person person = Person();
print(person.introduction()); // I don't have a favorite color yet
person.favoriteColor = "blue";
print(person.introduction()); // BLUE
person.favoriteColor = null;
print(person.introduction()); // I don't have a favorite color anymore
} It sounds like the definition of stable as "never changing once it's set" is more equivalent to |
It is true that a However, that's not the same thing as a stable declaration (variable or getter): If a getter or variable is declared stable then it is guaranteed that this declaration as well as every overriding declaration will have the stability property (that is, if it is evaluated multiple times then we get the same value each time).
That's true. Stable getters are not relevant in that case, but shadow variables could be used (#1514). But anyone who wishes to use elements of a functional programming style would probably have many instance variables that are final, and they should probably be stable, because it's part of the intended design that the value doesn't change. So even though you could override such a final variable by a getter that returns different values from time to time (if it is just
It is of course possible for a getter to change the value that it returns ( |
So would you say, for my To that note, can you provide a real-world example for stable getters? My intuition is that it's a replacement for |
Yes, for that example I'd like to have class Person {
String? favoriteColor; // Can be null, can be mutated.
String introduction() {
if (shadow: favoriteColor == null) {
return "I don't have a favorite color yet/anymore";
} else {
return favoriteColor.toUpperCase(); // Promoted.
}
}
} For real-world usage, I just checked that I expect that almost all of those can be changed from For every variable/getter which is changed to That seems pretty real-worldish to me. |
Now that you mention Flutter, would this be a good example of when to use stable? class MyWidget extends StatelessWidget {
final Widget child; // --> stable Widget child
final String? label; // --> stable String? label
const MyWidget(this.child, this.label);
// build() here
} As an addition, is it fair to add that any class marked with |
For final TheType theName = noSuchMethod(Invocation.getter(#theName)); which will cache the first successful result returned (after successfully downcasting to I guess mockito codegen can do something similar. |
About stable expressions being an extension of potentially constant expressions ... maybe we could extend potentially constant expressions to also allow accessing stable getters of potentially constant values. |
Yep, that's what I just said earlier today during a discussion about final getters. It would need to be late, but with that it should just work. |
A note in passing. I believe the following is a program which defines a getter class A {
late final int _x;
// A getter with a block body containing exactly one return statement return
// e; which occurs at the top level of the function body and where e is stable
// is a stable declaration.
int get x {
late int cache;
try {
cache = _x;
} catch (e) {
_x = 99;
cache = 0;
}
final int r = cache;
// A final local variable is stable
return r;
}
}
void main() {
A a = A();
print(a.x);
print(a.x);
} I think that at a minimum the definition of stable expression needs to be adjusted such that it is closed under substitution of values for local variables (both final and mutable). |
Well in that case, wouldn't this technically count? class A {
int get b {
// Final local variable is stable
final int x = Random().nextBool() ? 1 : 2;
// Returning a stable expression is stable
return x;
}
} I thought the point of marking something as stable is that it gains |
This is not what is proposed above. The proposal above is attempting to define a sub-language which is statically guaranteed to always produce the same result, so no caching is required. |
Agree. For a getter to be stable, its returned value needs to be stable globally. A final local variable is stable within its scope. It's not the same variable the next time the same code gets executed. Which means we probably need a hierarchy of stability:
Or more generally: contextually stable, aka. Something with a context dependency gives the same result when executed within the "same context". A getter is not instance stable just because the expression it returns is locally stable. The expression had to be instance stable too. |
@leafpetersen wrote:
Good catch, that's indeed true! I fixed that loophole in the implementation of the lint which was used to investigate the breakage associated with the 'final getters' variant of this mechanism, but apparently I forgot to fix it here. Here is a shorter way to use the same loophole: class A {
final int get x {
final now = DateTime.now().millisecondsSinceEpoch;
return now;
}
}
void main() {
A a = A();
print(a.x);
/* ... do something to waste at least one millisecond */
print(a.x);
} We need to distinguish between expressions that are stable based on having the same object as We have to require that @Levi-Lesches wrote:
True, that's enough, too. @lrhn wrote:
Indeed. I think it's enough to separate out 'locally stable' which is what I called 'body stable' in the update of the proposal where I fixed this loophole. Consider a member In order to be able to promote a stable/final expression in the body of The other two concepts don't seem to be needed separately: A getter needs to return a stable expression in order to be stable (doesn't matter whether it is instance stable or globally stable). If |
Stable Getters
Promotion of fields is probably the most prominent request we have in response to the release of null safety. One approach to improve support for promotion could be to ensure that a particular getter will return the same value if invoked multiple times, that is, the getter is stable.
Note that the underlying concept is immutability: A stable getter will implement an immutable property of the given object (if it's an instance getter, otherwise it's just an immutable property). This is a concept which is useful both because it enables promotion, but also for a number of other reasons related to software correctness and readability.
The modifier
stable
can be specified on any final instance variable or instance getter declaration.When
stable
is present in a declaration where the syntax also admitsfinal
,final
is implied and may be omitted.A compile-time error occurs if an instance getter declaration directly or indirectly overrides another getter declaration, where each of them can be explicitly declared or implicitly induced by a variable declaration, when the overridden declaration is marked
stable
and the overriding declaration is not a stable declaration.In other words, when a getter has been marked stable then it and every override must be stable. Note that the overridden declaration can be in a superclass, including mixins, or it can be in an implemented class.
=> e
wheree
is stable is a stable declaration.return e;
ife
is a stable expression that does not contain any reference to a local variable, and if the body cannot complete normally (that is, it can't also return null implicitly).The modifier
stable
can be specified on a library getter, and on a getter markedstatic
and declared in a class, mixin, or extension. A compile-time error occurs if said getter does not have a stable body. The modifierstable
can also be specified on a variable that implicitly induces one of these kinds of getters.A stable expression is defined as follows:
this
,this
is a stable expression.e
is a stable expression with static typeT
andm
is a stable getter or a method in the interface ofT
thene.m
is stable.stable
is stable (with or withoutlate
).static
andstable
declared in a class, mixin, or extension is stable (with or withoutlate
).Never
is stable.e1
ande2
are stable ande1
has static typenum
,int
,double
, orString
, thene1 + e2
is stable; and ifb
,e1
, ande2
are stable, thenb ? e1 : e2
is stable.Note that if a final local variable declared in a function
f
occurs in the body of a function literal or local function declared inf
then it may have been created during any execution of the enclosing function body. So we can have two instances of() => x
wherex
is a final local variable declared in the body off
, and they can return different values because the function objects were obtained by evaluating the same function literal expression during different runs off
. However, it is still true that multiple evaluations ofx
in the same function literal body or local function body will yield the same value every time it is evaluated during the execution of a specific function object. So even here it's OK to say that the local variable is stable, and, e.g., allow promotions like() => x is int ? x.isEven : false
.We can now safely promote any stable expression. Here is a basic example:
As a design trade-off, we can choose to allow compilers to cache the value of any stable expression. The argument in favor of this approach is that it will allow code to be expressed using the straightforward expressions (like
x.y
) and have good performance (because the value is cached into an implicitly created local variable). The argument against doing it is that it may be confusing for developers that an expression likex.y
isn't evaluated again and again when its value is used multiple times in the code, because it is implicitly turned into an occurrence of that implicitly created local variable.Final Getters
This is a variant of the proposal above: It does not introduce a new keyword
stable
. It is focused on the same concept, but strives to make that concept the semantics of existing syntax. As a result, it is a breaking change, because it makes certain existing programs an error (when a final variable declaration is overridden by a getter that isn't stable). But it recognizes the claim which has been made several times about stable getters: "That's the way it should have worked from the beginning".In this variant, a getter can be explicitly declared to be
final
(inspired by this comment):The modifier
final
on a getter implies that it is enforced that multiple invocations of that getter on the same receiver will evaluate to the same object (it may also throw, but it won't return two different objects).The getter of a variable declaration that includes
final
is a final getter. This is the breaking change, but the assumption is that it will not break much code, because such getters are rarely overridden by a getter that may return different values for different invocations with the same receiver.In order to allow the old semantics to be expressed, we introduce the following new syntax: The getter of an instance variable declaration that includes
final
followed byvar
is not a final getter.A compile-time error occurs if an instance getter declaration directly or indirectly overrides another getter declaration, where each of them can be explicitly declared or implicitly induced by a variable declaration, when the overridden declaration is a final getter, and the overriding declaration can not be a final getter.
In other words, when a getter is final then it and every override of it must be final. Note that the overridden declaration can be in a superclass, including mixins, or it can be in an implemented class.
var
implicitly induces a final getter.var
does not induce a final getter.)=> e
wheree
is a final expression can be a final getter.return e;
ife
is a final expression that does not contain any reference to a local variable, and if the body cannot complete normally (that is, it can't also return null implicitly).A final expression is defined as follows:
this
,this
is a final expression.e
is a final expression with static typeT
andm
is a final getter or a method in the interface ofT
thene.m
is final.late
).static
andfinal
, declared in a class, mixin, or extension is final (with or withoutlate
).Never
is final.e1
ande2
are final ande1
has static typenum
,int
,double
, orString
, thene1 + e2
is final; and ifb
,e1
, ande2
are final, thenb ? e1 : e2
is final.Note that if a final local variable declared in a function
f
occurs in the body of a function literal or local function declared inf
then it may have been created during any execution of the enclosing function body. So we can have two instances of() => x
wherex
is a final local variable declared in the body off
, and they can return different values because the function objects were obtained by evaluating the same function literal expression during different runs off
. However, it is still true that multiple evaluations ofx
in the same function literal body or local function body will yield the same value every time it is evaluated during the execution of a specific function object. So even here it's OK to say that the local variable is final, and, e.g., allow promotions like() => x is int ? x.isEven : false
.We can now promote any final expression. Here is a basic example:
As a design trade-off, we can choose to allow compilers to cache the value of any final expression. The argument in favor of this approach is that it will allow code to be expressed using the straightforward expressions (like
x.y
) and have good performance (because the value is cached into an implicitly created local variable). The argument against doing it is that it may be confusing for developers that an expression likex.y
isn't evaluated again and again when its value is used multiple times in the code, because it is implicitly turned into an occurrence of that implicitly created local variable.Note that this proposal makes it a breaking change to change a top-level or static variable from a final variable to a non-final getter. However, this is justified because it actually changes the semantics of the declarations. For instance, a final top-level variable can be cached and/or promoted, but the non-final getter cannot be cached nor promoted, and the reasoning that developers need to apply when they are writing code that uses it will also need to be reconsidered if we change the variable to a non-final getter. On the other hand, it is not a breaking change to change a final top-level variable to a final getter, i.e., we do not get boxed into the implementation choice of making it a variable.
[Edit Mar 14 2021: Fleshed out the definition of stable expressions. Mar 21: Added more stable expression forms. Apr 7: Require
stable
modifier on library/class variables for them to be stable. Edit Jun 28 2021: Wrote the 'Final Getters' variant of this proposal. Edit Sep 10 2023: Fix a mistake reported by @leafpetersen: The body of a stable/final getter cannot return an expression that contains a local variable.]The text was updated successfully, but these errors were encountered: