Skip to content

Commit fdc0b73

Browse files
authored
Update the primary constructors proposal (#3836)
Make adjustments to the proposal about primary constructors such that it is in line with the results from discussions at the recent physical language team meeting. In particular, remove support for a `primary` constructor which is declared in the body of the class/enum/etc, and only allow for the form which is part of the header of the class declaration. Also remove the ability to infer the modifier `required` in a formal parameter declaration. (Inferred `required` is now a separate proposal, see https://github.com/dart-lang/language/blob/main/working/0015-infer-required/feature-specification.md.) The ability to omit `final` from every parameter when the primary constructor is constant is preserved because we already have the ability to omit `final` in the primary constructor of an extension type declaration.
1 parent 122dfd7 commit fdc0b73

11 files changed

+141
-174
lines changed

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

Lines changed: 118 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ one constructor and a set of instance variables to be specified in a concise
1313
form in the header of the declaration. In order to use this feature, the given
1414
constructor must satisfy certain constraints, e.g., it cannot have a body.
1515

16-
A primary constructor can also be declared in the body of a class or
17-
similar declaration, using the modifier `primary`, in which case it can
18-
have an initializer list and a body, and it still has the ability to
19-
introduce instance variable declarations implicitly.
20-
2116
One variant of this feature has been proposed in the [struct proposal][],
2217
several other proposals have appeared elsewhere, and prior art exists in
2318
languages like [Kotlin][kotlin primary constructors] and Scala (with
@@ -106,9 +101,7 @@ and normal instance variable declarations, and it is probably a useful property
106101
that the primary constructor uses a formal parameter syntax which is completely
107102
like that of any other formal parameter list.
108103

109-
Just use a normal declaration and use an initializing formal in a primary
110-
constructor to initialize it from the primary constructor, if needed. An
111-
`external` instance variable amounts to an `external` getter and an
104+
An `external` instance variable amounts to an `external` getter and an
112105
`external` setter. Such "variables" cannot be initialized by an
113106
initializing formal anyway, so they will just need to be declared using a
114107
normal `external` variable declaration.
@@ -254,10 +247,6 @@ class Point {
254247
class Point(int x, {required int y});
255248
```
256249

257-
In this declaration it is possible to omit the modifier `required` on the
258-
named parameter `y`, because it is implied by the fact that the type of `y`
259-
is non-nullable (potentially non-nullable is enough).
260-
261250
The class header can have additional elements, just like class headers
262251
where there is no primary constructor:
263252

@@ -270,92 +259,10 @@ class D<TypeVariable extends Bound> extends A with M implements B, C {
270259
}
271260
272261
// Using a primary constructor.
273-
class const D<TypeVariable extends Bound>.named(int x, [int y = 0])
274-
extends A with M implements B, C;
275-
```
276-
277-
In the case where the header gets unwieldy it is possible to declare the
278-
primary constructor in the body of the class using the `primary` keyword:
279-
280-
```dart
281-
// Current syntax.
282-
class D<TypeVariable extends Bound> extends A with M implements B, C {
283-
final int x;
284-
final int y;
285-
const D.named(this.x, [this.y = 0]);
286-
}
287-
288-
// Using a primary constructor.
289-
class D<TypeVariable extends Bound> extends A with M implements B, C {
290-
primary const D.named(int x, [int y = 0]);
291-
}
292-
```
293-
294-
This approach offers more flexibility in that a primary constructor in the
295-
body of the declaration can have initializers and a body, just like other
296-
constructors. In other words, `primary` on a constructor has one effect
297-
only, which is to introduce instance variables for formal parameters in the
298-
same way as a primary constructor in the header of the declaration. For
299-
example:
300-
301-
```dart
302-
// Current syntax.
303-
class A {
304-
A(String _);
305-
}
306-
307-
class E extends A {
308-
LongTypeExpression x1;
309-
LongTypeExpression x2;
310-
LongTypeExpression x3;
311-
LongTypeExpression x4;
312-
LongTypeExpression x5;
313-
LongTypeExpression x6;
314-
LongTypeExpression x7;
315-
LongTypeExpression x8;
316-
external int y;
317-
int z;
318-
final List<String> w;
319-
320-
E({
321-
required this.x1,
322-
required this.x2,
323-
required this.x3,
324-
required this.x4,
325-
required this.x5,
326-
required this.x6,
327-
required this.x7,
328-
required this.x8,
329-
required this.y,
330-
}) : z = 1,
331-
w = const <Never>[],
332-
super('Something') {
333-
// A normal constructor body.
334-
}
335-
}
336-
337-
// Using a primary constructor.
338-
class E extends A {
339-
external int y;
340-
int z;
341-
final List<String> w;
342-
343-
primary E({
344-
LongTypeExpression x1,
345-
LongTypeExpression x2,
346-
LongTypeExpression x3,
347-
LongTypeExpression x4,
348-
LongTypeExpression x5,
349-
LongTypeExpression x6,
350-
LongTypeExpression x7,
351-
LongTypeExpression x8,
352-
this.y,
353-
}) : z = 1,
354-
w = const <Never>[],
355-
super('Something') {
356-
// A normal constructor body.
357-
}
358-
}
262+
class const D<TypeVariable extends Bound>.named(
263+
int x, [
264+
int y = 0
265+
]) extends A with M implements B, C;
359266
```
360267

361268
## Specification
@@ -367,64 +274,46 @@ for extension type declarations, because they're intended to use primary
367274
constructors as well.
368275

369276
```
370-
<topLevelDefinition> ::=
371-
<classDeclaration>
372-
| <extensionTypeDeclaration> // New alternative.
373-
| ...;
374-
375277
<classDeclaration> ::= // First alternative modified.
376278
(<classModifiers> | <mixinClassModifiers>)
377279
'class' <classNamePart> <superclass>? <interfaces>? <classBody>
378280
| ...;
379281
282+
<primaryConstructorNoConst> ::= // New rule.
283+
<typeIdentifier> <typeParameters>?
284+
('.' <identifierOrNew>)? <formalParameterList>
285+
286+
<classNamePartNoConst> ::= // New rule.
287+
<primaryConstructorNoConst>
288+
| <typeWithParameters>;
289+
380290
<classNamePart> ::= // New rule.
381-
'const'? <constructorName> <typeParameters>? <formalParameterList>
291+
'const'? <primaryConstructorNoConst>
382292
| <typeWithParameters>;
293+
294+
<typeWithParameters> ::= <typeIdentifier> <typeParameters>?
383295
384296
<classBody> ::= // New rule.
385297
'{' (<metadata> <classMemberDeclaration>)* '}'
386298
| ';';
387299
388-
<extensionTypeDeclaration> ::=
389-
'extension' 'type' 'const'? <typeWithParameters>
390-
<representationDeclaration>
391-
<interfaces>?
300+
<extensionTypeDeclaration> ::= // Modified rule.
301+
'extension' 'type' <classNamePart> <interfaces>?
392302
<extensionTypeBody>;
393303
394-
<representationDeclaration> ::=
395-
('.' <identifierOrNew>)? '(' <metadata> <type> <identifier> ')';
396-
397304
<extensionTypeMemberDeclaration> ::= <classMemberDeclaration>;
398305
399306
<extensionTypeBody> ::=
400307
'{' (<metadata> <extensionTypeMemberDeclaration>)* '}'
401308
| ';';
402309
403310
<enumType> ::= // Modified rule.
404-
'enum' <classNamePart> <mixins>? <interfaces>? '{'
311+
'enum' <classNamePartNoConst> <mixins>? <interfaces>? '{'
405312
<enumEntry> (',' <enumEntry>)* (',')?
406313
(';' (<metadata> <classMemberDeclaration>)*)?
407314
'}';
408-
409-
<methodSignature> ::=
410-
'primary'? <constructorSignature> <initializers>
411-
| 'primary'? <factoryConstructorSignature>
412-
| ... // Other cases unchanged.
413-
| 'primary'? <constructorSignature>;
414-
415-
<declaration> ::=
416-
... // Other cases unchanged.
417-
| 'primary'? <redirectingFactoryConstructorSignature>
418-
| 'primary'? <constantConstructorSignature> (<redirection> | <initializers>)?
419-
| 'primary'? <constructorSignature> (<redirection> | <initializers>)?;
420315
```
421316

422-
The word `type` is now used in the grammar, but it is not a reserved word
423-
or a built-in identifier. A parser that encounters the tokens `extension`
424-
and then `type` at a location where top-level declaration is expected shall
425-
commit to parsing it as an `<extensionTypeDeclaration>`. *This eliminates
426-
an ambiguity with `extension` (not `extension type`) declarations.*
427-
428317
A class declaration whose class body is `;` is treated as a class declaration
429318
whose class body is `{}`.
430319

@@ -446,10 +335,6 @@ extension type declaration without a primary constructor. An enum
446335
declaration with a primary constructor is desugared using the same
447336
steps. This determines the dynamic semantics of a primary constructor.
448337

449-
A compile-time error occurs if a class, extension type, or enum declaration
450-
has a primary constructor in the header as well as a constructor with the
451-
modifier `primary` in the body.
452-
453338
The following errors apply to formal parameters of a primary constructor.
454339
Let _p_ be a formal parameter of a primary constructor in a class `C`:
455340

@@ -507,8 +392,8 @@ then _k_ has the name `C`.
507392
If it exists, _D2_ omits the part derived from `'.' <identifierOrNew>` that
508393
follows the name and type parameter list, if any, in _D_.
509394

510-
_D2_ omits the formal parameter list _L_ that follows the name, type
511-
parameter list, if any, and `.id`, if any.
395+
Moreover, _D2_ omits the formal parameter list _L_ that follows the name,
396+
type parameter list, if any, and `.id`, if any.
512397

513398
The formal parameter list _L2_ of _k_ is identical to _L_, except that each
514399
formal parameter is processed as follows.
@@ -519,9 +404,7 @@ parameters preserve the name and the modifier `required`, if any. An
519404
optional positional or named parameter remains optional; if it has a
520405
default value `d` in _L_ then it has the transformed default value `_n` in
521406
_L2_, where `_n` is the name of the constant variable created for that
522-
default value. Finally, if `p` is an optional named parameter in _L_ with
523-
no default value whose type is potentially non-nullable then `required` is
524-
added to `p` in _L2_.
407+
default value.
525408

526409
- An initializing formal parameter *(e.g., `this.x`)* is copied from _L_ to
527410
_L2_, using said transformed default value, if any, and otherwise
@@ -543,13 +426,6 @@ added to `p` in _L2_.
543426

544427
Finally, _k_ is added to _D2_, and _D_ is replaced by _D2_.
545428

546-
Assume that _D_ is a class, extension type, or enum declaration in the
547-
program that includes a constructor declaration _k_ in the body which has
548-
the modifier `primary`. In this case, no transformations are applied to the
549-
default values of formal parameters of _k_, but otherwise the formal
550-
parameters of _k_ are processed in the same way as they are with a primary
551-
constructor in the declaration header.
552-
553429
### Discussion
554430

555431
It could be argued that primary constructors should support arbitrary
@@ -607,11 +483,6 @@ constructor using `D.named`, and that would fail if we use the approach
607483
where it occurs as `new.named` or `const.named` because that particular
608484
constructor has been expressed as a primary constructor.
609485

610-
A variant of this idea, from Leaf, is that we could allow one constructor
611-
in a class with no primary constructor in the header to be marked as a
612-
"primary constructor in the body". This proposal has now been made part
613-
of the proposal.
614-
615486
A proposal which was mentioned during the discussions about primary
616487
constructors was that the keyword `final` could be used in order to specify
617488
that all instance variables introduced by the primary constructor are
@@ -635,8 +506,104 @@ class Point {
635506
class final Point(int x, int y); // Not supported!
636507
```
637508

509+
Finally, we could allow a primary constructor to be declared in the body of
510+
a class or similar declaration, using the modifier `primary`, in which case
511+
it could have an initializer list and a body, and it would still have the
512+
ability to introduce instance variable declarations implicitly:
513+
514+
```dart
515+
// Current syntax.
516+
class D<TypeVariable extends Bound> extends A with M implements B, C {
517+
final int x;
518+
final int y;
519+
const D.named(this.x, [this.y = 0]);
520+
}
521+
522+
// Using a primary constructor in the class body.
523+
class D<TypeVariable extends Bound> extends A with M implements B, C {
524+
primary const D.named(int x, [int y = 0]);
525+
}
526+
```
527+
528+
This approach offers more flexibility in that a primary constructor in the
529+
body of the declaration can have initializers and a body, just like other
530+
constructors. In other words, `primary` on a constructor has one effect
531+
only, which is to introduce instance variables for formal parameters in the
532+
same way as a primary constructor in the header of the declaration. For
533+
example:
534+
535+
```dart
536+
// Current syntax.
537+
class A {
538+
A(String _);
539+
}
540+
541+
class E extends A {
542+
LongTypeExpression x1;
543+
LongTypeExpression x2;
544+
LongTypeExpression x3;
545+
LongTypeExpression x4;
546+
LongTypeExpression x5;
547+
LongTypeExpression x6;
548+
LongTypeExpression x7;
549+
LongTypeExpression x8;
550+
external int y;
551+
int z;
552+
final List<String> w;
553+
554+
E({
555+
required this.x1,
556+
required this.x2,
557+
required this.x3,
558+
required this.x4,
559+
required this.x5,
560+
required this.x6,
561+
required this.x7,
562+
required this.x8,
563+
required this.y,
564+
}) : z = 1,
565+
w = const <Never>[],
566+
super('Something') {
567+
// A normal constructor body.
568+
}
569+
}
570+
571+
// Using a primary constructor in the class body.
572+
class E extends A {
573+
external int y;
574+
int z;
575+
final List<String> w;
576+
577+
primary E({
578+
required LongTypeExpression x1,
579+
required LongTypeExpression x2,
580+
required LongTypeExpression x3,
581+
required LongTypeExpression x4,
582+
required LongTypeExpression x5,
583+
required LongTypeExpression x6,
584+
required LongTypeExpression x7,
585+
required LongTypeExpression x8,
586+
required this.y,
587+
}) : z = 1,
588+
w = const <Never>[],
589+
super('Something') {
590+
// A normal constructor body.
591+
}
592+
}
593+
```
594+
595+
We may get rid of all those occurrences of `required` in the situation
596+
where it is a compile-time error to not have them, but that is a
597+
[separate proposal][inferred-required].
598+
599+
[inferred-required]: https://github.com/dart-lang/language/blob/main/working/0015-infer-required/feature-specification.md
600+
638601
### Changelog
639602

603+
1.2 - May 24, 2024
604+
605+
* Remove support for primary constructors in the body of a declaration.
606+
640607
1.1 - August 22, 2023
641608

642609
* Update to refer to extension types rather than inline classes.

working/2364 - primary constructors/scripts/inline_class.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"isInline": true,
2+
"isExtensionType": true,
33
"name": "Point",
44
"fields": [
55
{

working/2364 - primary constructors/scripts/inline_const_class.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"isInline": true,
2+
"isExtensionType": true,
33
"name": "Point",
44
"isConst": true,
55
"fields": [

working/2364 - primary constructors/scripts/inline_generic_class.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"isInline": true,
2+
"isExtensionType": true,
33
"name": "Point",
44
"typeParameters": "<TypeVariable extends Bound>",
55
"fields": [

0 commit comments

Comments
 (0)