Skip to content

Commit 62d384f

Browse files
authored
Revise "declaring constructors". (#4169)
Revise "declaring constructors". - Apply review feedback from Lasse. - Add section for inferring public parameter names from private ones. - Update grammar to allow `var` followed by a type. - Add lint for using `final` on parameters. - Specify the semantics not in terms of field parameters.
1 parent 1d8d2b4 commit 62d384f

File tree

1 file changed

+240
-21
lines changed

1 file changed

+240
-21
lines changed

working/declaring-constructors/feature-specification.md

Lines changed: 240 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Declaring Constructors
22

3-
Author: Bob Nystrom (based on ideas by Lasse, Nate, et al)
3+
Author: Bob Nystrom (based on ideas by Erik, Lasse, Nate, et al)
44

55
Status: In-progress
66

7-
Version 0.1 (see [CHANGELOG](#CHANGELOG) at end)
7+
Version 0.2 (see [CHANGELOG](#CHANGELOG) at end)
88

99
Experiment flag: declaring-constructors
1010

@@ -222,9 +222,9 @@ But for the 1/3 of fields that are mutable, primary constructors avoid the `var`
222222
that this proposal requires.
223223

224224
There are real brevity advantages to primary constructors. But that brevity
225-
comes with trade-offs. A primary constructor can't have a body, initializer
226-
list, of explicit superclass constructor call. If you need any of those, you're
227-
back to needing a regular in-body constructor.
225+
comes with trade-offs. A primary constructor can't have a body and some versions
226+
of the proposal prohibit initializer lists, or explicit superclass constructor
227+
calls. If you need those, you're back to needing a regular in-body constructor.
228228

229229
A declaring constructor like in this proposal *is* an in-body constructor, so
230230
it has none of those limitations. A class author can *always* make one of a
@@ -236,10 +236,16 @@ proposal because they have a body, explicit initializer list, etc. (On the other
236236
hand, the fact that ~77% of constructors could still be primary constructors and
237237
be even *more* terse than this proposal is a point in favor of that proposal.)
238238

239-
I like to think of this feature as syntactic sugar for *instance fields*, not
240-
constructors. A class whose constructor initializes 80 fields will find either
241-
of these proposals 80 times more useful than a class with just one field. If
242-
you look at a corpus and count fields, the numbers are a little different.
239+
I like to think of this feature as more about *instance fields and parameters*
240+
than constructors themselves (beyond the tiny sweetness of `this` instead of a
241+
class name). By that I mean that the brevity scales with the number of *fields*
242+
that use it, not the number of constructors. A class whose constructor
243+
initializes 80 fields will find either of these proposals 80 times more useful
244+
than a class with just one field.
245+
246+
So when analyzing a corpus, it makes sense to look at *fields* that will use the
247+
feature instead of *constructors*. If you look at a corpus and count fields, the
248+
numbers are a little different.
243249

244250
In the same corpus, a little more than half (~53%) of all instance fields could
245251
be implicitly declared using a primary constructor. Around ~65% could be
@@ -253,11 +259,79 @@ with factory constructors, redirecting constructors, initializing formals, and
253259
super parameters, the amount of constructor-related syntactic sugar is getting a
254260
little silly.
255261

262+
### Private field parameters and initializing formals
263+
264+
While we're touching constructors, we have the opportunity to fix a
265+
long-standing annoyance. Initializing formals are common and well-loved, but
266+
they fail when you want to initialize a *private* field using a *named*
267+
parameter. The field's name starts with `_`, which isn't allowed for a named
268+
parameter. Instead, you are forced to write explicit initializers like:
269+
270+
```dart
271+
class House {
272+
int? _windows;
273+
int? _bedrooms;
274+
int? _swimmingPools;
275+
276+
House({
277+
int? windows,
278+
int? bedrooms,
279+
int? swimmingPools,
280+
}) : _windows = windows,
281+
_bedrooms = bedrooms,
282+
_swimmingPools = swimmingPools;
283+
}
284+
```
285+
286+
Note also that the author is forced to type annotate the parameters as well
287+
since they are no longer inferred from the initialized field.
288+
289+
When I last [analyzed a corpus][corpus private], 17% of all field initializers
290+
in initializer lists were doing nothing but shaving off a `_`. There is an
291+
obvious intended semantics here: simply remove the `_` from the named parameter
292+
but keep it for the initialized field. Likewise, for declaring constructors,
293+
the induced field keeps the `_` while the parameter name loses it. That turns
294+
the above example into:
295+
296+
[corpus private]: https://github.com/dart-lang/language/blob/db9f63185707c4c89a69118e842e4cc6e0e59cc3/resources/instance-initialization-analysis.md
297+
298+
```dart
299+
class House {
300+
int? _windows;
301+
int? _bedrooms;
302+
int? _swimmingPools;
303+
304+
House({this._windows, this._bedrooms, this._swimmingPools});
305+
}
306+
```
307+
308+
And when combined with a declaring constructor:
309+
310+
```dart
311+
class House {
312+
this({
313+
var int? _windows,
314+
var int? _bedrooms,
315+
var int? _swimmingPools,
316+
});
317+
}
318+
```
319+
320+
While this is a tiny sprinkle of syntactic sugar, it has a deeper value.
321+
Initializing formals and declaring constructors are so much more concise that
322+
users will want to use them whenever they can. But if they don't support private
323+
fields and named parameters, then we are incentivizing users to make instance
324+
fields public that they might otherwise prefer to keep private.
325+
326+
It's a well-established software engineering principle to minimize public state,
327+
so we don't want the language to discourage users from encapsulating fields.
328+
256329
## Syntax
257330

258331
The syntax changes are small—just using `this` instead of the class name in a
259-
constructor. But weaving it into the grammar is a little complicated because
260-
some kinds of constructors can't be declaring:
332+
constructor and allowing `var` on a parameter along with a type. But weaving it
333+
into the grammar is a little complicated because some kinds of constructors
334+
can't be declaring:
261335

262336
```
263337
classMemberDeclaration ::=
@@ -278,11 +352,44 @@ the parameter. A declaring constructor can be `const` or not. It can't be
278352
redirecting. It also can't be `external` since an external constructor has no
279353
implicit initializer list or body where the fields could be initialized.*
280354

355+
We also need to allow `var` before a simple formal parameter while also allowing
356+
a type annotation. (Today, `var` is allowed, but only in place of a type, like
357+
how variables are declared.) We redefine `simpleFormalParameter` to:
358+
359+
```
360+
simpleFormalParameter ::=
361+
'covariant'? ('final' | 'var')? type? identifier
362+
```
363+
364+
*The `simpleFormalParameter` rule was previously defined in terms of the same
365+
rules used for variable declarations. That meant that the grammar for parameters
366+
allowed `late` and `const`. Those are then disallowed by the specification
367+
outside of the grammar. Here, we eliminate the need for that extra-grammatical
368+
restriction by defining a grammar specifically for simple formal parameters that
369+
only includes what they allow.*
370+
371+
*We could allow `late` on field parameters and have that apply to the instance
372+
field (but not the parameter since parameters are always initialized). That
373+
could be useful in theory if there are other generative constructors that don't
374+
initialize the field. But to avoid confusion, we simply don't allow it. If a
375+
user wants a `late` instance field, they can always declare it outside of the
376+
declaring constructor.*
377+
378+
It is a compile-time error for a `simpleFormalParameter` to have both `var`
379+
and a type outside of a declaring constructor.
380+
381+
*This keeps the normal parameter list grammar consistent with other variable
382+
declarations. We could allow `var int x` as a parameter outside of a declaring
383+
constructor, but doing so would be confusing because it looks like a field
384+
parameter but isn't. Ideally, we would also disallow `final` on parameters
385+
outside of declaring constructors, but doing so is a breaking change.*
386+
281387
## Static semantics
282388

283389
This feature is just syntactic sugar for things the user can already express,
284390
so there are no interesting new semantics.
285391

392+
### Declaring constructors
286393

287394
Given a `declaringConstructor` D in class C:
288395

@@ -291,16 +398,28 @@ Given a `declaringConstructor` D in class C:
291398
* If P has a `final` or `var` modifier, it is a **field parameter**:
292399

293400
* Implicitly declare an instance field F on the surrounding class with
294-
the same name and type as P. *If P has no type, it implicitly has
295-
type `dynamic`, as does F.*
401+
the same name and type as P. *If P has no type annotation, it
402+
implicitly has type `dynamic`, as does F.*
296403

297404
* If P is `final`, then the instance field is also `final`.
298405

299-
* Any doc comments and metadata annotations on P are also copied to F.
300-
*For example, a user could mark the parameter `@override` if the
301-
the implicitly declared field overrides an inherited getter.*
406+
* Any doc comments and metadata annotations on P are also applied to
407+
F. *For example, a user could mark the parameter `@override` if the
408+
the implicitly declared field overrides an inherited getter. If a
409+
user wants to document the instance field induced by a field
410+
parameter, they can do so by putting a doc comment on the
411+
parameter.*
412+
413+
*P now behaves like an initializing formal that initializes F.
414+
Concretely:*
302415

303-
* P comes an initializing formal that initializes F.
416+
* A final local variable with the same name and type as P is
417+
introduced into the formal parameter initializer scope *(the scope
418+
that the initializer list has access to but the constructor body
419+
does not)*.
420+
421+
* Parameter P is *not* bound in the formal parameter scope of D.
422+
*Inside the body, references to P's name refer to F, not P.*
304423

305424
*Note that a declaring constructor doesn't have to have any field
306425
parameters. A user still may want to use the feature just to use `this`
@@ -320,7 +439,7 @@ It is a compile-time error if:
320439
* The implicitly declared fields would lead to an erroneous class. *For
321440
example if the class has a `const` constructor but one of the field
322441
parameters induces a non-`final` field, or an induced field collides with
323-
another member of the same name.
442+
another member of the same name.*
324443

325444
* An implicitly declared field is also explicitly initialized in the declaring
326445
constructor's initializer list. *This is really just a restatement of the
@@ -329,12 +448,79 @@ It is a compile-time error if:
329448

330449
* A field parameter is named `_`. *We could allow this but... why?*
331450

451+
### Private field parameters and initializing formals
452+
453+
An identifier is a *private name* if it starts with an underscore (`_`),
454+
otherwise it's a *public name*.
455+
456+
A private name may have a *corresponding public name*. If the characters of the
457+
identifier with the leading underscore removed form a valid identifier and a
458+
public name, then that is the private name's corresponding public name. *For
459+
example, the corresponding public name of `_foo` is `foo`.* If removing the
460+
underscore does not leave something which is is a valid identifier *(as in `_`
461+
or `_2x`)* or leaves another private name *(as in `__x`)*, then the private name
462+
has no corresponding public name.
463+
464+
The private declared name, *p*, of an initializing formal or field parameter in
465+
constructor C has a corresponding *non-conflicting public name* if it has a
466+
corresponding public name, *n*, and no other parameter of the same constructor
467+
declaration has either of the names *p* or *n* as declared name. *In other
468+
words, if removing the `_` leads to a collision with another parameter, then
469+
there is a conflict.*
470+
471+
Given an initializing formal or field parameter with private name *p*:
472+
473+
* If *p* has a non-conflicting public name *n*, then:
474+
475+
* The name of the parameter in C is *n*. *If the parameter is named, this
476+
then avoids the compile-time error that would otherwise be reported for
477+
a private named parameter.*
478+
479+
* The local variable in the initializer list scope of C is *p*. *Inside
480+
the body of the constructor, uses of *p* refer to the field, not the
481+
parameter.*
482+
483+
* If the parameter is an initializing formal, then it initializes a
484+
corresponding field with name *p*.
485+
486+
* Else the field parameter induces an instance field with name *p*.
487+
488+
*Any generated API documentation for the parameter should also use *n*.*
489+
490+
* Else (there is no non-conflicting public name), the name of the parameter is
491+
left alone and also used for the initialized or induced field. *If the
492+
parameter is named, this is a compile-time error.*
493+
494+
*For example:*
495+
496+
```dart
497+
class Id {
498+
late final int _region = 0;
499+
500+
this({this._region, final int _value}) : assert(_region > 0 && _value > 0);
501+
502+
@override
503+
String toString() => 'Id($_region, $_value)';
504+
}
505+
506+
main() {
507+
print(Id(region: 1, value: 2)); // Prints "Id(1, 2)".
508+
}
509+
```
510+
332511
## Runtime semantics
333512

334-
There are no runtime semantics for this feature.
513+
The runtime semantics for field parameters inside a declaring constructor are
514+
the same as for initializing formals:
515+
516+
Executing a field parameter with name *id* causes the instance variable *id*
517+
of the immediately surrounding class to be assigned the value of the
518+
corresponding actual argument.
335519

336520
## Compatibility
337521

522+
### Declaring constructors
523+
338524
The identifier `this` is already a reserved word that can't appear at this
339525
point in the grammar, so this is a non-breaking change that doesn't affect any
340526
existing code.
@@ -363,10 +549,19 @@ a *very* small number of authors use.)
363549
So while the potential for confusion is there, I think it's unlikely to be a
364550
problem in practice.
365551

552+
### Private field parameters and initializing formals
553+
554+
Any existing initializing formals with private names must be positional since
555+
it's a compile-time error to have a private named parameter. Since those
556+
arguments are passed positionally, the change to give the parameter a public
557+
name has no effect on any callsites.
558+
559+
Generated documentation may change, but that should be harmless.
560+
366561
### Language versioning
367562

368-
Even this change it's non-breaking, it is language versioned and can only be
369-
used in libraries whose language version is at or later than the version this
563+
Even though this change is non-breaking, it is language versioned and can only
564+
be used in libraries whose language version is at or later than the version this
370565
feature ships in. This is mainly to ensure that users don't inadvertently try to
371566
use this feature in packages whose SDK constraint allows older Dart SDK versions
372567
that don't support the feature.
@@ -409,8 +604,32 @@ become a declaring one:
409604
declaring? Probably the one with the most initializing formals, but what do
410605
you do in case there's a tie?
411606

607+
### Lint for `final` parameters
608+
609+
This proposal retcons the `final` modifier on parameters to mean something
610+
specific in a declaring constructor. Outside of a declaring constructor the
611+
modifier is allowed but has a different effect: it simply makes the parameter
612+
itself non-assignable.
613+
614+
If declaring constructors become popular, then users will likely start to see
615+
`final` before a parameter and read it as a field parameter. They will then be
616+
confused if the parameter isn't actually in a declaring constructor and isn't
617+
a field parameter.
618+
619+
Given that non-assignable parameters aren't actually that *useful*, it may be
620+
worth discouraging users from marking a parameter with `final` unless it is a
621+
field parameter. This seems like a good candidate for a lint.
622+
412623
## Changelog
413624

625+
### 0.2
626+
627+
- Apply review feedback from Lasse.
628+
- Add section for inferring public parameter names from private ones.
629+
- Update `simpleFormalParameter` grammar to allow `var` followed by a type.
630+
- Add lint for using `final` on parameters.
631+
- Specify the semantics not in terms of field parameters.
632+
414633
### 0.1
415634

416635
- Initial draft.

0 commit comments

Comments
 (0)