Skip to content

Commit a530cd6

Browse files
authored
Update primary constructor proposal based on email thread (#4161)
Update the primary constructors feature spec to support something like the features mentioned by Lasse in email on Nov 8th: Allow initializer lists to have `this.x = e` and `super.name(...)` elements, not just assertions, and specify that a variable `x` which is used in an initializing element (of the form `this.v = ...x...` or `v = ...x...`) or in a superinitializer (`super(...x...)`) does not introduce an instance variable in the class.
1 parent 48d3cbd commit a530cd6

File tree

1 file changed

+142
-126
lines changed

1 file changed

+142
-126
lines changed

working/2364 - primary constructors/feature-specification.md

Lines changed: 142 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Author: Erik Ernst
44

55
Status: Draft
66

7-
Version: 1.3
7+
Version: 1.4
88

99
Experiment flag: primary-constructors
1010

@@ -81,16 +81,15 @@ The basic idea is that a parameter list that occurs just after the class
8181
name specifies both a constructor declaration and a declaration of one
8282
instance variable for each formal parameter in said parameter list.
8383

84-
A primary constructor cannot have a body, and it cannot have an normal
85-
initializer list (and hence, it cannot have a superinitializer, e.g.,
86-
`super.name(...)`). However, it can have assertions, it can have
87-
initializing formals (`this.p`) and it can have super parameters
88-
(`super.p`).
84+
A primary constructor cannot have a body. However, it can have assertions,
85+
it can have initializing formals (`this.p`), it can have super parameters
86+
(`super.p`), and it can have an initializer list.
8987

90-
The motivation for these restrictions is that a primary constructor is
91-
intended to be small and easy to read at a glance. If more machinery is
92-
needed then it is always possible to express the same thing as a body
93-
constructor (i.e., any constructor which isn't a primary constructor).
88+
The motivation for the missing support for a body is that a primary
89+
constructor is intended to be small and easy to read at a glance. If more
90+
machinery is needed then it is always possible to express the same thing as
91+
a body constructor (i.e., any constructor which isn't a primary
92+
constructor).
9493

9594
The parameter list uses the same syntax as constructors and other functions
9695
(specified in the grammar by the non-terminal `<formalParameterList>`).
@@ -211,7 +210,7 @@ class const Point(int x, int y);
211210
enum E(String s) { one('a'), two('b') }
212211
```
213212

214-
Finally, an extension type declaration is specified to use a
213+
Note that an extension type declaration is specified to use a
215214
primary constructor (in that case there is no other choice,
216215
it is in the grammar rules):
217216

@@ -276,7 +275,7 @@ class const D<TypeVariable extends Bound>.named(
276275
]) extends A with M implements B, C;
277276
```
278277

279-
Finally, it is possible to specify assertions on a primary constructor,
278+
It is possible to specify assertions on a primary constructor,
280279
just like the ones that we can specify in the initializer list of a
281280
regular (not primary) constructor:
282281

@@ -292,6 +291,48 @@ class Point {
292291
class Point(int x, int y): assert(0 <= x && x <= y * y);
293292
```
294293

294+
Finally, it is possible to use an initializer list in order to
295+
invoke a superconstructor and/or initialize some explicitly
296+
declared instance variables with a computed value.
297+
298+
```dart
299+
// Current syntax.
300+
301+
class A {
302+
final int x;
303+
const A.someName(this.x);
304+
}
305+
306+
class B extends A {
307+
final String s1;
308+
final String s2;
309+
310+
const B(int x, int y, {required this.s2})
311+
: s1 = y.toString(), super.someName(x + 1);
312+
}
313+
314+
// Using primary constructors.
315+
316+
class const A.someName(int x);
317+
318+
class const B(int x, int y, {required String s2})
319+
: s1 = y.toString(), assert(s2.isNotEmpty), super.someName(x + 1)
320+
extends A {
321+
final String s1;
322+
}
323+
```
324+
325+
A formal parameter of a primary constructor which is used in a variable
326+
initialization or in a superinitializer does not implicitly induce an
327+
instance variable.
328+
329+
In particular, `int x` does not give rise to an instance variable in the
330+
class `B` because `x` is used in the superinitializer. Similarly, `int y`
331+
does not give rise to an instance variable because it is used in the
332+
initializer list element `s1 = y.toString()`. However, `s2` _does_ give
333+
rise to an instance variable (it is used in the assertion, but that does
334+
not prevent the instance variable from being induced).
335+
295336
## Specification
296337

297338
### Syntax
@@ -309,19 +350,16 @@ constructors as well.
309350
<primaryConstructorNoConst> ::= // New rule.
310351
<typeIdentifier> <typeParameters>?
311352
('.' <identifierOrNew>)? <formalParameterList>
312-
<assertions>?
313-
314-
<assertions> ::= // New rule.
315-
':' <assertion> (',' <assertion>)*
353+
<initializers>?
316354
317355
<classNamePartNoConst> ::= // New rule.
318356
<primaryConstructorNoConst>
319357
| <typeWithParameters>;
320-
358+
321359
<classNamePart> ::= // New rule.
322360
'const'? <primaryConstructorNoConst>
323361
| <typeWithParameters>;
324-
362+
325363
<typeWithParameters> ::= <typeIdentifier> <typeParameters>?
326364
327365
<classBody> ::= // New rule.
@@ -378,8 +416,11 @@ and `final`. *A final instance variable cannot be covariant, because being
378416
covariant is a property of the setter.*
379417

380418
Conversely, it is not an error for the modifier `covariant` to occur on
381-
other formal parameters of a primary constructor (this extends the
382-
existing allowlist of places where `covariant` can occur).
419+
another formal parameter _p_ of a primary constructor (this extends the
420+
existing allowlist of places where `covariant` can occur), unless _p_
421+
occurs in an initializer element of the primary constructor which is not
422+
an assertion. *For example, `class C(covariant int p): super(p + 1);` is an
423+
error.*
383424

384425
The desugaring consists of the following steps, where _D_ is the class,
385426
extension type, or enum declaration in the program that includes a primary
@@ -393,56 +434,28 @@ Where no processing is mentioned below, _D2_ is identical to _D_. Changes
393434
occur as follows:
394435

395436
Assume that `p` is an optional formal parameter in _D_ which is not an
396-
initializing formal and not a super parameter. Assume that `p` does not
397-
have a declared type, but it does have a default value whose static type in
398-
the empty context is a type (not a type schema) `T` which is not `Null`. In
399-
that case `p` is considered to have the declared type `T`. When `T` is
400-
`Null`, `p` is considered to have the declared type `Object?`. If `p`
401-
does not have a declared type nor a default value then `p` is considered
402-
to have the declared type `Object?`.
437+
initializing formal, and not a super parameter. Assume that `p` does not
438+
occur in the initializer list of _D_, except possibly in some assertions.
439+
Assume that `p` does not have a declared type, but it does have a default
440+
value whose static type in the empty context is a type (not a type schema)
441+
`T` which is not `Null`. In that case `p` is considered to have the
442+
declared type `T`. When `T` is `Null`, `p` is considered to have the
443+
declared type `Object?`. If `p` does not have a declared type nor a default
444+
value then `p` is considered to have the declared type `Object?`.
403445

404446
*Dart has traditionally assumed the type `dynamic` in such situations. We
405447
have chosen the more strictly checked type `Object?` instead, in order to
406448
avoid introducing run-time type checking implicitly.*
407449

408-
The current scope of the formal parameter list of the primary constructor
409-
in _D_ is the type parameter scope of the enclosing class, if it exists,
410-
and otherwise the enclosing library scope *(in other words, the default
411-
values cannot see declarations in the class body)*.
412-
413-
*Note that every occurrence of a type variable of _D_ in a default value is
414-
an error, because no constant expression contains a type variable. Hence,
415-
we can proceed under the assumption that there are no such occurrences.*
450+
The current scope of the formal parameter list and initializer list (if
451+
any) of the primary constructor in _D_ is the body scope of the class.
416452

417453
*We need to ensure that the meaning of default value expressions is
418-
well-defined, taking into account that the primary constructor is actually
419-
located in a different scope than normal non-primary constructors. One way
420-
to specify this is to use a syntactic transformation:*
421-
422-
Every default value in the primary constructor of _D_ is replaced by a
423-
fresh private name `_n`, and a constant variable named `_n` is added to the
424-
top-level of the current library, with an initializing expression which is
425-
said default value.
426-
427-
*This means that we can move the parameter declarations including the
428-
default value without changing its meaning. Implementations are free to
429-
use this particular desugaring based technique, or any other technique
430-
which has the same observable behavior. In particular, it should not be
431-
possible for such a default value to obtain a new meaning because an
432-
identifier in the default value resolves to a declaration in the class body
433-
when it occurs in _k_ after the transformation, but it used to resolve to
434-
a top-level or imported declaration before the transformation.*
435-
436-
For each of these constant variable declarations, the declared type is the
437-
formal parameter type of the corresponding formal parameter, except: In the
438-
case where the corresponding formal parameter has a type `T` where one or
439-
more type variables declared by _D_ occur, the declared type of the
440-
constant variable is the least closure of `T` with respect to the type
441-
parameters of the class.
442-
443-
*For example, if the default value is `const []` and the parameter type is
444-
`List<X>`, the top-level constant will be `const List<Never> _n = [];` for
445-
some fresh name `_n`.*
454+
well-defined, taking into account that the primary constructor is
455+
physically located in a different scope than normal non-primary
456+
constructors. We do this by specifying the current scope explicitly as the
457+
body scope, in spite of the fact that the primary constructor is actually
458+
placed outside the braces that delimit the class body.*
446459

447460
Next, _k_ has the modifier `const` iff the keyword `const` occurs just
448461
before the name of _D_, or _D_ is an `enum` declaration.
@@ -461,23 +474,25 @@ type parameter list, if any, and `.id`, if any.
461474
The formal parameter list _L2_ of _k_ is identical to _L_, except that each
462475
formal parameter is processed as follows.
463476

464-
In particular, the formal parameters in _L_ and _L2_ occur in the same
465-
order, and mandatory positional parameters remain mandatory, and named
466-
parameters preserve the name and the modifier `required`, if any. An
467-
optional positional or named parameter remains optional; if it has a
468-
default value `d` in _L_ then it has the transformed default value `_n` in
469-
_L2_, where `_n` is the name of the constant variable created for that
470-
default value.
477+
The formal parameters in _L_ and _L2_ occur in the same order, and
478+
mandatory positional parameters remain mandatory, and named parameters
479+
preserve the name and the modifier `required`, if any. An optional
480+
positional or named parameter remains optional; if it has a default value
481+
`d` in _L_ then it has the default value `d` in _L2_ as well.
471482

472483
- An initializing formal parameter *(e.g., `this.x`)* is copied from _L_ to
473-
_L2_, using said transformed default value, if any, and otherwise
474-
unchanged.
475-
- A super parameter is copied from _L_ to _L2_ using said transformed
476-
default value, if any, and is otherwise unchanged.
477-
- A formal parameter (named or positional) of the form `T p` or `final T p`
478-
where `T` is a type and `p` is an identifier is replaced in _L2_ by
479-
`this.p`. A parameter of the same form but with a default value uses said
480-
transformed default value.
484+
_L2_, along with the default value, if any, and is otherwise unchanged.
485+
- A super parameter is copied from _L_ to _L2_ along with the default
486+
value, if any, and is otherwise unchanged.
487+
- Assume that _p_ is a formal parameter (named or positional) of the form
488+
`T p` or `final T p` where `T` is a type and `p` is an identifier.
489+
Assume that _p_ occurs in the initializer list of _D_, in an element
490+
which is not an assertion. In this case, _p_ occurs without changes in
491+
_L2_. *Note that the parameter cannot be covariant in this case, that is
492+
an error.*
493+
- Otherwise, a formal parameter (named or positional) of the form `T p` or
494+
`final T p` where `T` is a type and `p` is an identifier is replaced in
495+
_L2_ by `this.p`, along with its default value, if any.
481496
Next, an instance variable declaration of the form `T p;` or `final T p;`
482497
is added to _D2_. The instance variable has the modifier `final` if the
483498
parameter in _L_ is `final`, or _D_ is an `extension type` declaration,
@@ -487,48 +502,45 @@ default value.
487502
removed from the parameter in _L2_, and it is added to the instance
488503
variable declaration named `p`.
489504

490-
If there are any assertions following the formal parameter list _L_ then
491-
_k_ has an initializer list with the same assertions in the same order.
492-
493-
The current scope of the assertions in _D_ is the formal parameter
494-
initializer scope of the formal parameter list *(that is, they can see the
495-
parameters including any initializing formals, the type parameters, and
496-
everything in the library scope that isn't shadowed by the scopes in
497-
between)*.
505+
If there is an initializer list following the formal parameter list _L_ then
506+
_k_ has an initializer list with the same elements in the same order.
498507

499-
The expressions in the assertions are subject to a transformation that
500-
preserves the resolution of every identifier in said expressions when they
501-
occur as part of the initializer list of _k_. *(In particular, an
502-
identifier in an assertion expression cannot resolve to a declaration in
503-
the class body)*.
508+
*The current scope of the initializer list in _D_ is the body scope
509+
of the enclosing declaration, which means that they preserve their
510+
semantics when moved into the body.*
504511

505512
Finally, _k_ is added to _D2_, and _D_ is replaced by _D2_.
506513

507514
### Discussion
508515

509-
It could be argued that primary constructors should support arbitrary
510-
superinvocations using the specified superclass:
516+
It could be argued that primary constructors should not support
517+
superinitializers because the resulting declaration is too complex to be
518+
conveniently readable, and developers could just write a regular primary
519+
constructor instead.
520+
521+
We expect that primary constructors will in practice be small and simple,
522+
but they may use different subsets of the expressive power of the
523+
mechanism. For example,
524+
511525

512526
```dart
513-
class B extends A { // OK.
514-
B(int a): super(a);
515-
}
527+
// Use super parameters.
516528
517-
class B(int a) extends A(a); // Could be supported, but isn't!
518-
```
529+
class const Point2D(int x, int y);
530+
531+
class const Point3D(super.x, super.y, int z) extends Point2D;
532+
533+
// Use a named constructor and a computed super argument.
519534
520-
There are several reasons why this is not supported. First, primary
521-
constructors should be small and easy to read. Next, it is not obvious how
522-
the superconstructor arguments would fit into a mixin application (e.g.,
523-
when the superclass is `A with M1, M2`), or how readable it would be if the
524-
superconstructor is named (`class B(int a) extends A with M1, M2.name(a);`).
525-
For instance, would it be obvious to all readers that the superclass is `A`
526-
and not `A.name`, and that all other constructors than the primary
527-
constructor will ignore the implied superinitialization `super.name(a)` and
528-
do their own thing (which might be implicit)?
535+
class A._(int x);
529536
530-
In short, if you need to write a complex superinitialization like
531-
`super.name(e1, otherName: e2)` then you need to use a body constructor.
537+
class B(int y): assert(y > 2), super._(y - 1)
538+
extends A with Mixin1, Mixin2;
539+
```
540+
541+
Like many other language mechanisms, primary constructors need developers
542+
to use their human judgment to create declarations that are both readable,
543+
useful, and maintainable.
532544

533545
There was a [proposal from Bob][] that the primary constructor should be
534546
expressed at the end of the class header, in order to avoid readability
@@ -598,17 +610,15 @@ class Point {
598610
class final Point(int x, int y); // Not supported!
599611
```
600612

601-
Most likely, there is an easy workaround: Make the constructor `const`. It
602-
is very often possible to make the constructor `const`, even in the case
603-
where the class isn't necessarily intended to be used in constant
604-
expressions: There is no initializer list, no superinitialization, no
605-
body. The only way it can be an error to use `const` on a primary
606-
constructor is if the superclass doesn't have a constant constructor, or if
607-
the class has a mutable or late instance variable, or it has some
608-
non-constant expressions in instance variable declarations. (Those issues
609-
can only be created by instance variables that are declared explicitly in
610-
the class body whereas the ones that are created by primary constructor
611-
parameters will necessarily satisfy the `const` requirements).
613+
There is an easy partial workaround: Make the constructor `const`. It is
614+
very often possible to make the constructor `const`, even in the case where
615+
the class isn't necessarily intended to be used in constant expressions:
616+
There is no body. The only ways it can be an error to use `const` on a
617+
primary constructor is if the superclass doesn't have a constant
618+
constructor, or if the class has a mutable or late instance variable, or it
619+
has some non-constant expressions in instance variable declarations or in
620+
the initializer list. Using `const` is not a complete solution, but
621+
probably OK in practice.
612622

613623
Finally, we could allow a primary constructor to be declared in the body of
614624
a class or similar declaration, possibly using a modifier like `primary`,
@@ -631,11 +641,10 @@ class D<TypeVariable extends Bound> extends A with M implements B, C {
631641
```
632642

633643
This approach offers more flexibility in that a primary constructor in the
634-
body of the declaration can have initializers and a body, just like other
635-
constructors. In other words, `primary` on a constructor has one effect
636-
only, which is to introduce instance variables for formal parameters in the
637-
same way as a primary constructor in the header of the declaration. For
638-
example:
644+
body of the declaration can have a body, just like other constructors. In
645+
other words, `primary` on a constructor has one effect only, which is to
646+
introduce instance variables for formal parameters in the same way as a
647+
primary constructor in the header of the declaration. For example:
639648

640649
```dart
641650
// Current syntax.
@@ -698,13 +707,20 @@ class E extends A {
698707
```
699708

700709
We may get rid of all those occurrences of `required` in the situation
701-
where it is a compile-time error to not have them, but that is a
710+
where it is a compile-time error to not have them, but that is a
702711
[separate proposal][inferred-required].
703712

704713
[inferred-required]: https://github.com/dart-lang/language/blob/main/working/0015-infer-required/feature-specification.md
705714

706715
### Changelog
707716

717+
1.4 - November 12, 2024
718+
719+
* Add support for a full initializer list (which adds elements of the form
720+
`x = e` and `super(...)` or `super.name(...)`). Add the rule that a
721+
parameter introduces an instance variable except when used in the
722+
initializer list.
723+
708724
1.3 - July 12, 2024
709725

710726
* Add support for assertions in the primary constructor. Add support for

0 commit comments

Comments
 (0)