Skip to content

Commit 98341cd

Browse files
committed
Update records proposal:
- Remove the reflective static members on `Record`. - Remove support for single positional element records. - Remove synthesized getters for positional fields. Close #1275. Close #1277. Close #1291.
1 parent 91da80e commit 98341cd

File tree

1 file changed

+77
-64
lines changed

1 file changed

+77
-64
lines changed

working/0546-patterns/records-feature-specification.md

Lines changed: 77 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Author: Bob Nystrom
44

55
Status: In progress
66

7-
Version 1.3 (see [CHANGELOG](#CHANGELOG) at end)
7+
Version 1.4 (see [CHANGELOG](#CHANGELOG) at end)
88

99
## Motivation
1010

@@ -50,7 +50,8 @@ var tuple = ("first", 2, true);
5050
```
5151

5252
A tuple is an ordered list of unnamed positional fields. These languages also
53-
often have **record** types. In a record, the fields are unordered, but named:
53+
often have **record** types. In a record, the fields are unordered and
54+
identified by name instead:
5455

5556
```dart
5657
var record = (number: 123, name: "Main", type: "Street");
@@ -63,26 +64,29 @@ record has a series of positional fields, and a collection of named fields:
6364
var record = (1, 2, a: 3, b: 4);
6465
```
6566

66-
Very much like an argument list to a function call both in syntax and semantics.
67-
A given record may have no positional fields or no named fields, but cannot be
68-
totally empty. (There is no "unit type".)
69-
70-
A record expression like the above examples produces a record value. This is a
67+
The expression syntax looks much like an argument list to a function call. A
68+
record expression like the above examples produces a record value. This is a
7169
first-class object, literally a subtype of Object. Its fields cannot be
7270
modified, but may contain references to mutable objects. It implements
7371
`hashCode` and `==` structurally based on its fields to provide value-type
7472
semantics.
7573

74+
A record may have only positional fields or only named fields, but cannot be
75+
totally empty. *There is no "unit type".* A record with no named fields must
76+
have at least two positional fields. *This prevents confusion around whether a
77+
single positional element record is equivalent to its underlying value, and
78+
avoids a syntactic ambiguity with parenthesized expressions.*
79+
7680
## Core library
7781

7882
These primitive types are added to `dart:core`:
7983

8084
### The `Record` class
8185

8286
A built-in class `Record` with no members except those inherited from `Object`.
83-
This type cannot be constructed, extended, mixed in, or implemented by
84-
user-defined classes. *It's similar to how the `Function` class is the
85-
superclass for function types.*
87+
All record types are a subtype of this class. This type cannot be constructed,
88+
extended, mixed in, or implemented by user-defined classes. *This is similar to
89+
how the `Function` class is the superclass for all function types.*
8690

8791
## Syntax
8892

@@ -92,29 +96,28 @@ A record is created using a record expression, like the examples above. The
9296
grammar is:
9397

9498
```
95-
// Existing rule:
9699
literal ::= record
97-
| // Existing literal productions...
100+
| // Existing literal productions...
98101
record ::= '(' recordField ( ',' recordField )* ','? ')'
99102
recordField ::= (identifier ':' )? expression
100103
```
101104

102105
This is identical to the grammar for a function call argument list. There are a
103-
couple of syntactic restrictions not captured by the grammar. A parenthesized
104-
expression without a trailing comma is ambiguously either a record or grouping
105-
expression. To resolve the ambiguity, it is always treated as a grouping
106-
expression.
106+
couple of syntactic restrictions not captured by the grammar. It is a
107+
compile-time error if a record has any of:
107108

108-
It is a compile-time error if a record has any of:
109+
* The same field name more than once.
109110

110-
* the same field name more than once.
111+
* No named fields and only one positional field. *This avoids ambiguity with
112+
parenthesized expressions.*
111113

112-
* a field name that collides with the implicit name defined for a
113-
positional field (see below).
114+
* A field named `hashCode`, `runtimeType`, `noSuchMethod`, or `toString`.
114115

115-
* a field named `hashCode`, `runtimeType`, `noSuchMethod`, or `toString`.
116-
117-
**TODO: Can field names be private? If so, are they actually private?**
116+
* A field name that starts with an underscore. *If we allow a record to have
117+
private field names, then those fields would not be visible outside of the
118+
library where the record was declared. That would lead to a record that has
119+
hidden state. Two such records might unexpectedly compare unequal even
120+
though all of the fields the user can see are equal.*
118121

119122
### Record type annotations
120123

@@ -123,9 +126,8 @@ record type annotations is:
123126

124127
```
125128
// Existing rule:
126-
typeNotVoidNotFunction ::= typeName typeArguments? '?'?
127-
| 'Function' '?'?
128-
| recordType // New production.
129+
typeNotVoidNotFunction ::= recordType
130+
| // Existing typeNotVoidNotFunction productions...
129131
130132
recordType ::= '(' recordTypeFields ','? ')'
131133
| '(' ( recordTypeFields ',' )?
@@ -139,15 +141,28 @@ recordTypeNamedFields ::= '{' recordTypeNamedField
139141
recordTypeNamedField ::= type identifier
140142
```
141143

142-
This is somewhat similar to a parameter list. You have zero or more positional
143-
fields where each field is a type annotation:
144+
It is a compile-time error if a record type has any of:
145+
146+
* The same field name more than once.
147+
148+
* No named fields and only one positional field. *This isn't ambiguous, since
149+
there are no parenthesized type expressions in Dart. But there is no reason
150+
to allow single positional element record types when the corresponding
151+
record values are prohibited.*
152+
153+
* A field named `hashCode`, `runtimeType`, `noSuchMethod`, or `toString`.
154+
155+
* A field name that starts with an underscore.
156+
157+
The syntax is similar to a function type's parameter list. You have zero or more
158+
positional fields where each field is a type annotation:
144159

145160
```dart
146161
(int, String, bool) triple;
147162
```
148163

149-
Then an optional brace-delimited section for named fields. Each named field is
150-
a type and name pair:
164+
Then a brace-delimited section for named fields. Each named field is a type and
165+
name pair:
151166

152167
```dart
153168
({int n, String s}) pair;
@@ -168,12 +183,6 @@ parentheses:
168183

169184
Like record expressions, a record type must have at least one field.
170185

171-
Unlike expressions, a trailing comma is not required in the single positional
172-
field case. `(int)` is a valid record type and is distinct from the type `int`.
173-
174-
It is a compile-time error if two record type fields have the same name or if
175-
a named field collides with the implicit name of a positional field.
176-
177186
## Static semantics
178187

179188
We define **shape** to mean the number of positional fields (the record's
@@ -193,18 +202,16 @@ A record type declares all of the members defined on `Object`. It also exposes
193202
getters for each named field where the name of the getter is the field's name
194203
and the getter's type is the field's type.
195204

196-
In addition, for each positional field, the record type declares a getter named
197-
`field<n>` where `<n>` is the number of preceding positional fields and where
198-
the getter's type is the field's type.
205+
Positional fields are not exposed as getters. *Record patterns in pattern
206+
matching can be used to access a record's positional fields.*
199207

200-
For example, the record expression `(1, s: "string", true)` has a record type
201-
whose signature is like:
208+
For example, the record expression `(1.2, name: 's', true, count: 3)` has a
209+
record type whose signature is like:
202210

203211
```dart
204-
class {
205-
int get field0;
206-
String get s;
207-
bool get field1;
212+
class extends Record {
213+
String get name;
214+
int get count;
208215
}
209216
```
210217

@@ -228,7 +235,7 @@ of the corresponding field in the original types.
228235
```dart
229236
(num, String) a = (1.2, "s");
230237
(int, Object) b = (2, true);
231-
var c = cond ? a : b; // (num, Object)
238+
var c = cond ? a : b; // c has type `(num, Object)`.
232239
```
233240

234241
Likewise, the greatest lower bound of two record types with the same shape is
@@ -237,15 +244,15 @@ the greatest lower bound of their component fields:
237244
```dart
238245
a((num, String)) {}
239246
b((int, Object)) {}
240-
var c = cond ? a : b; // Function((int, String))
247+
var c = cond ? a : b; // c has type `Function((int, String))`.
241248
```
242249

243250
The least upper bound of two record types with different shapes is `Record`.
244251

245252
```dart
246253
(num, String) a = (1.2, "s");
247254
(num, String, bool) b = (2, "s", true);
248-
var c = cond ? a : b; // Record
255+
var c = cond ? a : b; // c has type `Record`.
249256
```
250257

251258
The greatest lower bound of records with different shapes is `Never`.
@@ -260,16 +267,6 @@ fields are) and collection literals.
260267

261268
## Runtime semantics
262269

263-
### The `Record` type
264-
265-
The `positionalFields()` method takes a record and returns an `Iterable` of all
266-
of the record's positional fields in order.
267-
268-
The `namedFields()` method takes a record and returns a Map with entries for
269-
each named field in the record where each key is the field's name and the
270-
corresponding value is the value of that field. (The methods are static to avoid
271-
colliding with fields in an actual record object.)
272-
273270
### Records
274271

275272
#### Members
@@ -284,17 +281,18 @@ The `toString()` method's behavior is unspecified.
284281

285282
Records behave similar to other primitive types in Dart with regards to
286283
equality. They implement `==` such that two records are equal iff they have the
287-
same shape and all corresponding pairs of fields are equal (determined using
288-
`==`).
284+
same shape and all corresponding pairs of fields are equal. Fields are compared
285+
for equality by calling `==` on the corresponding field values in the same
286+
order that `==` was called on the records.
289287

290288
```dart
291-
var a = (1, 2);
292-
var b = (1, 2);
289+
var a = (x: 1, 2);
290+
var b = (2, x: 1);
293291
print(a == b); // true.
294292
```
295293

296-
The implementation of `hashCode` follows this. Two records that are equal have
297-
the same hash code.
294+
The implementation of `hashCode` follows this. Two records that are equal must
295+
have the same hash code.
298296

299297
#### Identity
300298

@@ -362,6 +360,21 @@ covariant in their field types.
362360

363361
## CHANGELOG
364362

363+
### 1.4
364+
365+
- Remove the reflective static members on `Record`. Like other reflective
366+
features, supporting these operations may incur a global cost in generated
367+
code size for unknown benefit (#1275, #1277).
368+
369+
- Remove support for single positional element records. They don't have any
370+
current use and are a syntactic wart. If we later add support for spreading
371+
argument lists and single element positional records become useful, we can
372+
re-add them then.
373+
374+
- Remove synthesized getters for positional fields. This avoids problems if a
375+
positional field's synthesized getter collides with an explicit named field
376+
(#1291).
377+
365378
### 1.3
366379

367380
- Remove the `Destructure_n_` interfaces.

0 commit comments

Comments
 (0)