dart-lang / dart-lang/language
Rename for initializing formals (and similar parameters)
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
This is an alternative proposition to the https://github.com/dart-lang/language/issues/2509 issue.
The base issue is that `this._foo` is a good way to initialize a private variable, but it also makes the private name of the field be the name of the public-facing constructor parameter. That’s annoying/ugly for positional parameters, and prevents using the initializing formal entirely for named parameters, which cannot have private names. The “solution” is to not use an initializing formal, and do:
```dart
class C {
final int _foo, _bar;
C(int foo, {required int bar}) : _foo = foo, _bar = bar;
}
```
rather than using initializing formals.
The other solution is to ignore the problem for positional variables and do `C(this._foo)`. It doesn’t look good in the documentation, but it works, since names of positional parameters are not important outside of the declaration.
With “primary constructors”, as currently defined, a “parameter” declaration of `final int foo` introduces an instance variable `final int foo;` and treats the parameter declaration as `this.foo`. To introduce a private-named instance variable, you‘d again need to have a public-facing private name, and it cannot be a named parameter. (For inline classes, as currently defined, primary constructors would likely almost exclusively be introducing private instance variables.) Primary constructors do not have initializer lists, so there is no rewrite, the alternative is to not use a primary constructor at all, if you want to initialize any private-name instance variable.
The underlying issue is that the same declaration, which has just one identifier, actually denotes two different things, which do not, and should not, necessarily have the same name.
So, a proposal for allowing you to explicitly change the name of a parameter whose declared name identifier is also used to reference something else.
## Rename operator
Allow an initializing formal to declare a name. The name of the initialized field is just the *default*, which you get if you don't specify one yourself, just like the type of the initialized field is the default type, but you can write another type if you want to.
```dart
C(this._x as x, {required thix._y as y});
```
The `as name` occurs after the parameter, before a default value if the parameter is optional. It applies to all currently allowed parameter shapes.
It can also be applied to `super.x` when used as a named parameter. (This is a little more questionable, because it allows changing how a parameter is passed to the super-constructor, but only in a very limited way, by renaming. It doesn't allow you to change a positional parameter into a name super-constructor argument or vice-versa. So why allow this particular, limited change. Basically, because we can. We can also choose to not allow it.)
The syntax is not needed for function *types*, and does not apply to such. It only applies to parameter lists of actual functions or constructors.
The syntax is only allowed for parameters where the same name would otherwise have two jobs, `this.x` and named-parameter `super.x`. It should extend to primary constructor field-introducing parameter declarations too.
#### Parsing
There should be no ambiguity in parsing existing `this.x` or `super.x` parameters followed by `as identifier`.
Examples:
```dart
// Constructors this.x or super.x parameters
C(this.x as y);
C({super.x as y});
C({int this.x as y = 0});
C(int this.x(int v)? as y); // Old-style function syntax.
```
For primary constructor parameters, which also introduce instance variables, there is not necessarily a `this` or `super` to synchronize on:
```dart
class C({covariant final num x as y = 0});
class C(SomeType x as y);
```
A primary constructor parameter must have a type or `var`/`final`, it cannot simply be `class C(x);`. _(Right?)_
The word `as` is a valid parameter name, it’s only a built-in identifier, but at least it’s not a valid type. The worst case example, `(int as as as)`, should still be parsable. The parser probably has to be very careful about not making premature decisions about what’s going on.
A bigger worry is that the syntax may conflict with allowing patterns as parameters. We may (IMO: definitely) want to allow a parameter to be declaration pattern, and allow initializing formals as binding patterns inside them, something like:
```dart
class Point {
final int x, y;
Point.fromPair(var (this.x, this.y));
// Works like:
// Point.fromPair((int, int) $tmp) : x = $tmp.$1, y = $tmp.$2;
}
```
We do not allow cast patterns (`p as SomeType`) in declaration patterns, meaning that `int x as y` cannot be a cast pattern in a parameter declaration position, so it should not conflict with `int x as rename`. It still means parsing has to know the context, and cannot just “parse any pattern, then check if it’s valid here”. And it means that the same syntax, `int x as y`, means different things in different places.
#### Pros
More flexible than, say, deriving a “corresponding public name” from a private-named initializing formal, since you can choose names with no relation, say a field named `_isEnabled` and a parameter named `enabled`. It can be used to rename public-named field initializers too.
All names occurring in the program have a declaring occurrence. That’s a good thing for both tools and users. The analyzer won’t have to find and maintain connections between different names. It can rename one or the other name independently.
Directly applies to a primary constructor parameters, allowing them to introduce a field with a different name than the constructor parameter:
```dart
class Floo({bool isEnabled as enabled});
```
#### Cons
Can be verbose and repetitive, when you need to write `this._veryLongName veryLongName`. _No way around that if we don’t want source names to be implicitly introduced. This is probably the minimal solution with that constraint._
Reuses an existing word, `as` which already have at least 2-3 meanings (import scope naming, and one or two type assertions, depending on whether you consider cast in expression and cast in pattern as the same thing.) Now it will have 3-4 meanings. It’s *consistent* with import scope naming in that it gives a name to something.
Contributor guide
Research direction
Read the linked issue 2509 and compare this proposal with the current rules for initializing formals and primary constructors. Trace the parsing and pattern-conflict questions in the Parsing section; done would require a settled design and corresponding language-specification changes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- dart
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100