-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Allow read-only access to initializing formals. #26655
Comments
I thought the plan was that the initializing formal is only visible in the initialization list. Putting it in scope in the body would shadow the field, which I think is a breaking change. Consider: class C {
String x;
C(this.x) {
x = "after";
}
}
main() {
var c = new C("before");
print(c.x);
} This prints "after" today but would be an error with the plan here, right? |
correct. |
@floitschG: can you clarify what you mean by "correct"? Shadowing the field in the constructor body (as described by @eernstg above) is indeed a breaking change, but it's the only reasonable semantics. Magically hiding the initializing formal parameter in the body would go against all scoping rules in Dart. We do have code in the dart library that breaks under the new semantics. For example:
|
From the spec's perspective, an initializing formal creates a local variable whose scope is the initializer list and which disappears before the constructor body, because, I think, that's the easiest way to spec it. From the user's perspective, though, what they will think they see is that the field is in scope during the initialization list (but can only be read from and not written to). Of course, that's not strictly true, because the "field" is accessible even before the What the user will see is what they expect to see, I think: a name that comes into scope at the constructor parameter list and stays in scope through the body. It's just that half-way through, it magically transforms from a read-only local variable to the actual field, in a way that's virtually invisible to them. |
I think magically switching The main usage where it matters (which is already a bit weird) is probably receiving the value of a field directly by an initializing formal, then later fixing it up to be something else in the body. Why not just receive the value as an explicit parameter and then compute the right value for that field in an initializer or in the body? But that case would be caught, because it is modifying a final parameter. A more tricky case is when the body contains a function literal that captures the field (now: the parameter), and the field gets modified later on. That's semantically breaking, and it is likely to go under the radar. Could we hint that this is a dangerous practice when we discover that the capture of an initializing formal parameter occurs? Is it ever likely to be exactly the right thing to do, or can we assume that "they should have captured the field, anyway" is always true? In all cases the problem can be handled by changing occurrences of To sum up, this is very much about detection of the breaking constructs, and we do have information about where they are at compile time. |
On Thu, Jun 9, 2016 at 12:05 AM Bob Nystrom notifications@github.com
That's not what I'd expect to see. The simple model that I would expect is If I wanted to refer to the field x in the body where it's shadowed by the As Erik notes, you can observe the difference by capturing 'x' in an |
It is a wart. For Dart 2.0 we might consider removing the wart and make the parameter scope cover the body. We can't do that right now since it would break existing constructors that take a |
It's about breaking existing code versus having rules that make sense, i.e., rules that developers will "get" rather than constantly getting confused about them. I tend to think that "gettable" rules is a very important criterion---for instance, all parameters have the same scope rules. Note that it's only a breaking change when the access is a capture or a modification (including |
OK, turn around, the matter has been resolved IRL: We'll let the initializing formal be in scope in the initializer list and not in the body, judging that the breakage of the rule is too heavy a burden. I'll adjust the wording of the initial comment in this issue. |
For Dart 1 the final parameter will only be visible in the initializer list. |
Another option is to drop this language change for 1.x and do it for 2.0 only. |
@eernstg I don't understand your comment stating the matter has been resolved, how was it resolved? |
Resolution: the final parameter is only visible in the initializer list. The reason this feature is only added now, is for exactly this reason: introducing a fresh variable leads to ambiguity which variable is referenced in the body. We are adding it now, because, after writing lots of Dart code, we feel that the benefit strongly outweighs that possible confusion. That decision hasn't changed because of the misunderstanding. It's still a feature we want to have in one of the next releases. |
Then I wouldn't call it a parameter, because it isn't. It's a variable local to the initializer list. In this code: class C {
var x;
var y;
C(x, this.x) : y = x;
} Does the final x in scope for the parameter list shadow the parameter x? Or does the x in the nested parameter list scope prevent a declaration of x in the outer scope? Also, why is it final? Assigning to it seems benign even if it's not necessarily useful and (most) variables in Dart are non-final by default. |
The spec states that an initializing formal parameter does not introduce a name into the scope where other parameters are provided. So some choice has to be made when it is added, including how to handle duplicates. In fact, the duplicate name in About the scoping, I agree that the story gets a bit more complex than my initial understanding, but I don't agree that it would help developers in general to introduce an extra layer and insist that The initializing formal parameter could be final, non-final, same-as-the-field, or controlled by an explicit |
The problem with making it final is that there is no real simple workaround if that's not what is intended. Before this change we had the equivalence: Now, the story is not so simple and the specified behavior doesn't seem like something that can be macro-expressed in terms of existing Dart --- so we really are adding a new semantic capability to the language. And it's not one that's generally applicable, but just for this restricted situation. (There's already a simple workaround anyway, as I noted just above.) |
Granted, the implicit finality is a restriction. However, usages where the intention is to modify the initializing formal argument will be new because that's a new feature. So the equivalence you mention wouldn't be used as a rewriting-rule, and in that case I believe that it would count as a reasonable workaround to declare a regular parameter and write the constructor with that in mind. We still need a good story about how the field can be in scope in the body and not the parameter, and I'd like that story to be simple and consistent. Maybe it would work to say that there are two separate scopes for the initializer list and the body, not nested but side-by-side, and only the one for the initializer list gets the initializing formals. As an example of a structure where it is reasonable for one construct to introduce several "sibling" scopes for its syntactic children you could consider a hypothetical typecase: typecase (e as x) { // `x` is a fresh variable capturing the value of e for the type test.
case int: y = x + 42; // `x` is in scope in this case, with the type int.
case String: x += "42"; // `x` is in scope in this case, with the type String.
...
default: .. // `x` might not even be in scope in the default block.
} |
In order to make an implementation of initializing formal access in `dart2js` available as specified in issue #26655 now, this CL changes the scope management such that initializing formals are not in scope in the constructor body, only in the initializer list. This is done by introducing a new notion of scopes implemented by `ExtensionScope`: Such a scope will extend an existing `NestedScope` rather than adding a new nested scope to it. R=johnniwinther@google.com Review URL: https://codereview.chromium.org/2059883002 .
The language team has created an informal language specification document pertaining to this feature which is available at https://gist.github.com/eernstg/cff159be9e34d5ea295d8c24b1a3e594. The feature is available in dart2js under the flag '--initializing-formal-access', and the CL https://codereview.chromium.org/2141023002/ will make the effect of '--initializing-formal-access' the default behavior of dart2js. Keeping such a "make it the default" CL ready for other tools will make it possible to enable this feature by default in a coordinated manner. |
|
Ad 1. Absolutely. Inside the initializer list, the variable corresponding to the initializing formal should work the same as if it has been a final parameter declaration with the same value. Any difference from that is a bug in the spec. (I really want to change it to being that in Dart 2.0, but that is still contentious :). Ad 2. It's a bug in the test. Ad 3. Didn't miss anything. It should be a static warning and a strong-mode error (but it should work fine at runtime in spec mode). |
Basic support added by https://codereview.chromium.org/2335693002/. There are probably some errors that we're not currently catching, but those should be easy to add. The feature can be enabled using either the |
That's great, thanks! The CL https://codereview.chromium.org/2332253003/ corrects initializing_formal_promotion_test to fit the current scope rules; initializing_formal_type_test cannot use multi-testing, so we need to find a different way to declare that the static warning is expected. |
When this issue was created it wasn't stated explicitly when each subtask would be complete: We could say that it's complete when the feature is working and available under a flag, or we could require also the step to make it available by default. Clearly, different teams have made different choices in this respect so far. So I've decided that this issue is considered complete for each tool when the feature is available under a flag (and I've added some checkmarks to reflect that interpretation --- please add more of them when appropriate), and I've created a new issue, #27891, to track the step where this feature is enabled by default. |
For convenience, and because it is non-disruptive, we wish to support read-only access to initializing formal arguments, that is, the
this.x
style arguments that generative constructors may have. An example:The details of this language change are as follows:
this.x
of a constructorC
introduces the namex
into a scope that coversC
's initializers, but not the body; it is considered to be a final parameter.The feature is currently available in
dart2js
under the flag--initializing-formal-access
(which will be removed such that the feature is available in general), and some tests are available in the filestests/language/initializing_formal_*_test.dart
.Tracking bugs for the individual tasks:
There are no tasks for the formatter and style guide because this change does not affect the syntax.
Edit: Note that the scope rule was changed such that the initializing formal is not in scope in the body, which means that existing usages of the field in the body will preserve their current semantics.
The text was updated successfully, but these errors were encountered: