-
Notifications
You must be signed in to change notification settings - Fork 209
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
[Field promotion] Final expressions #3327
Comments
I'm personally not a fan of this syntax. I find it pretty hard to read and unclear as to what exactly is going on here. It seems like there's a hidden variable being declared (though that's not really what's happening). That's why I like the if-case syntax because you have to explicitly declare a new variable inside the if parenthesis and it's more clear that the getter's value is being "held" because it's being stored in class Bad {
bool? get random => math.Random().nextBool() ? true : null;
void test(Bad other) {
if (other.random case final bool r) {
print(r); // always prints "true"
}
print("${other.random}"); // could print null or "true"
}
} |
I think it would be confusing to have expressions which look like property reads treated as a local variable. I think the |
My summary would be that: ... EDIT: that I completely misread the feature, so everything below not really useful. Snip. |
One other minor point against this syntax is that it requires you to write out the whole expression again like so: if (final x.foo.bar.baz is String) {
print(x.foo.bar.baz.codeUnitAt(0));
}
// When compared to
if (x.foo.bar.baz case final String baz) {
print(baz.codeUnitAt(0));
} |
Well, yes, but... we have that syntax and we still have people asking for field promotion. So manifestly it is not so.
This is not a point against it. This is explicitly what people are asking for when they ask for field promotion (which they are asking for). It is true that for extremely long paths they might choose to bind a variable, but that doesn't change the fact that empirically, users are requesting the ability to promote paths, are happy for the limited cases that we are providing, and are asking us to provide support for more cases. I got thoroughly lost in where you were going in your comment, but strictly looking at your summary:
Sort of?
I don't understand all of your conditionality here. Delete the lines starting "and they" and "but we", and I agree with the summary. All of the rest is irrelevant to this proposal.
But we can't. That's the whole point!
We might want to for other reasons, but yes, this is primarily aimed at promotion.
I have no idea what you're trying to say here.
I answered this in my initial comment.
We don't. I'm not sure where this is coming from? Nothing in the proposal says anything about promotion being needed. I'm really lost with the rest of your comment. I feel like maybe we got some wires crossed about what the proposed feature is? |
Ack, I completely missed the leading With the The syntax precisely matches part of So, this is a way to implicitly introduce a variable, and not give it a name. (Or saying that the "name" of a variable can be an entire expression, and the entire expression denotes that variable. Using plain identifier expression is just the simple case.) It definitely works, in that it can be given a consistent and sound semantics As you point out, it gets tricky to read if Restrict it to any getter on a stable receiver (in the field-promotion sense, where a final field getter would be promotable) seems reasonable. The final getter can still have side-effects. If we know that it cannot, the expression would likely be field-promotable already. I think it's OK to assume that we cache the value of that one call, but not as reasonable to assume that we cache an entire chain of member accesses up to it. That brings it very close to The one case where a stable receiver can change, is when it's only locally stable, because the root expression is a local variable. Field promotion can promote that, if it can see that the variable doesn't change between test and use. The bigger question then becomes where this test can occur. Is it only as the condition of an Can it be any expression? Only in conditions, or sub-condition? Can I do: if (final e.x is Foo && final e.x.foo != null) {
print("${e.x.bar} : ${e.x.foo.isValid});
} Even the following can make sense: var exists = (final e.x is Foo) && e.x.isNotEmpty; The usual options are:
I dislike the last option for not being orthogonal enough, and probably have a hard time selling the first, so something like the middle one would be what I'd go with. But I also think that a (slightly generalized) if (list[i] != null) foo(list[i]!); and it's not just because of the What I really would want to write in this case is That's a null check, a bail-out type assertion could be A pipe operator could do Or we can introduce a variable, and give users a good, short, inline syntax for that variable, rather than trying to make them repeat an entire expression, which still only works for some expressions (not for I'm warming up to |
Field reads cannot in general be promoted based on null checks or instance checks because stability of the result cannot be guaranteed. Older overview summary here. Various proposals have been floated to resolve this based on either providing a way for APIs to opt into promotion (e.g. stable getters) or by providing better ways to bind a local variable (search for the label field-promotion for some discussion issues).
This issue sketches out an approach based on introducing an implicit hidden final variable to cache the value of a field read within the scope of a promotion. We allow a promotable expression (for the purposes of this discussion, we can consider the set of syntactic expressions which are currently candidates for private field promotion) to be prefixed by
final
to indicate that within the "scope" (loosely speaking) of the expression, subsequent references to the expression should re-use the value of the initial evaluation of the expression, rather than re-computing the value. This guarantees stability of the expression, making promotion sound. As with private field promotion, assignments to the root of the path would need to be accounted for appropriately. Examples:An advantage of this approach is that it applies to any field or getter, without any requirements for the API designed to make a choice. It does so without requiring the user to invent, bind, and use a new local variable.
A disadvantage of this approach is that the "scope" of the hidden variable may not be obvious. This doesn't matter for "well-behaved" getters, but as the
Bad
example shows, if a getter actually returns different values on different reads, then which invocations are cached becomes relevant, and is not clearly visible in the syntax.One approach to mitigating this would be to only allow final expressions to be "bound" in a limited set of locations in which scope is clear (similar to the way we handle
if case
expressions now. So then we could say thatif (final e <predicate>) <block>
caches the value ofe
within<block>
, where<predicate>
is one of a limited set of binary operations (e.g.!= null
andis T
). This makes it clear what the scope of the "finalization" is. Example:There is still some possibility for confusion around assignments to the root of the path. For example, the following code might be confusing:
It seems wrong to allow the second call to
bad.x
in the body of theif
to use the cached value. We could say that this code is valid, but that the "finality" ends at the point of the assignment, but perhaps it is better to say that within the scope of a final expression, assignments to the root variable of the path are statically disallowed, and hence the code above becomes an error.With the adjustments above, we end up with something that essentially behaves as if
if (final e != null) <block>
were shorthand forif (e case final freshVar?) <block2>
whereblock2
isblock
with all uses ofe
replaced withfreshVar
.cc @dart-lang/language-team
The text was updated successfully, but these errors were encountered: