Skip to content

Commit 7282583

Browse files
authored
Iterate on proposal, allow generic tear-offs.
1 parent b687d3e commit 7282583

File tree

1 file changed

+63
-28
lines changed

1 file changed

+63
-28
lines changed
Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Dart Constructor Tear-offs
22

3-
Author: [email protected]<br>Version: 2.1
3+
Author: [email protected]<br>Version: 2.2
44

55
Dart allows you to tear off (aka. closurize) methods instead of just calling them. It does not allow you to tear off *constructors*, even though they are just as callable as methods (and for factory methods, the distinction is mainly philosophical).
66

@@ -14,30 +14,30 @@ This is a proposal for constructor tear-offs which introduces minimal new syntax
1414

1515
We allow tearing off named constructors.
1616

17-
An expression *e* of the form <code>*C*.*name*</code> where *C* is an identifier or qualified identifier denoting a class, and *name* is the base name of a named constructor of the class *C*, is currently allowed by the grammar, but rejected by the static semantics. It can occur as part of a constructor invocation, but cannot be an expression by itself because it has no value. We introduce a static and dynamic expression semantic for such a *named constructor tear-off expression*.
17+
An expression *e* of the form <code>*C*.*name*</code> where *C* is an identifier or qualified identifier denoting a class, and *name* is the base name of a named constructor of the class *C*, is currently allowed by the grammar, but rejected by the static semantics. It can occur as part of a constructor invocation, but cannot be an expression by itself because it has no value. We introduce a static and dynamic expression semantic for such a *named constructor tear-off expression*.
1818

19-
A named constructor tear-off expression of the form <code>*C*.*name*</code> evaluates to a function value which could be created by tearing off a *corresponding constructor function*, which would be a static function defined on the class denoted by *C*
19+
A named constructor tear-off expression of the form <code>*C*.*name*</code> evaluates to a function value which could be created by tearing off a *corresponding constructor function*, which would be a static function defined on the class denoted by *C*:
2020

21-
> static *C* *name*$tearoff(*params*) => *C*.*name*(*args*);
21+
> <code>static *C* *name*$tearoff\<*typeParams*>(*params*) => *C*\<*typeArgs*>.*name*(*args*);</code>
2222
23-
where *params* is a parameter list equivalent to the one for <code>*C*.*name*</code> (same optional/required parameters with the same names, types and default values, but where any initializing formals are replaced by normal parameters of the same type), and *args* is an argument list passing those parameters to `C.name` directly as they are received. For example, `Uri.http` evaluates to an expression which could have been created by a corresponding function literal expression:
23+
If *C* is not generic, the <code>\<*typeParams*\></code> and <code>\<*typeArgs*\></code> are omitted. Otherwise <code>\<*typeParams*\></code> are exactly the same type parameters as those of the class declaration of *C*, and <code>\<*typeArgs*></code> applies those type parameter variables directly as type arguments to *C*.
24+
25+
Similarly, <code>*params*</code> is almost exactly the same parameter list as the constructor *C*.*name*, except that *initializing formals* are represented by normal parameters with the same type. All remaining properties of the parameters are the same as for the corresponding constructor parameter, including any default values, and *args* is an argument list passing those parameters to `C.name` directly as they are received. For example, `Uri.http` evaluates to an expression which could have been created by a corresponding function literal expression:
2426

2527
```dart
2628
static Uri http$tearoff(String authority, String unencodedPath, [Map<String, dynamic>? queryParameters]) =>
2729
Uri.http(authority, unencodedPath, queryParameters);
2830
```
2931

30-
If the class denoted by *C* is generic, the corresponding constructor function is a generic function with the same type arguments (names and bounds) as the class. For example, the corresponding constructor function for `List.filled` would be:
32+
and a constructor of a generic class, like `List.filled`, would be:
3133

32-
```dart
34+
```6dart
3335
static List<E> filled$tearoff<E>(int count, E fill) => List<E>.filled(count, fill);
3436
```
3537

36-
When tearing off the constructor of a generic class, *the function tear-off is always instantiated* so the resulting function is not generic. This works the same way as instantiated tear-off of any other function, except that it is not an option to *not* instantiate when tearing off. If type inference has no constraints on the type arguments, they will be filled in by instantiate to bounds.
37-
38-
The static type of the named constructor tear-off expression is the same as the static type of the corresponding (instantiated) constructor function tear-off.
38+
When tearing off the constructor of a generic class, the result is precisely the same as if tearing off the corresponding static constructor function, including whether the expression is constant and how it canonicalizes, and whether a generic function is implicitly instantiated.
3939

40-
Also, similarly to function tear-offs, constructor tear-offs are potentially constant and canonicalized. Whenever the corresponding constructor function tear-off would be constant and canonicalized, the constructor tear-off itself is also constant and canonicalized.
40+
The static type of the named constructor tear-off expression is the same as the static type of the corresponding constructor function tear-off.
4141

4242
### Unnamed constructor tear-off
4343

@@ -48,65 +48,89 @@ Because of that, we introduce a *new* syntax that can be used to denote the unna
4848
```dart
4949
class C {
5050
final int x;
51-
const C.new(this.x); // declaration
51+
const C.new(this.x); // declaration.
5252
}
5353
class D extend C {
54-
D(int x) : super.new(x * 2); // super constructor reference
54+
D(int x) : super.new(x * 2); // super constructor reference.
5555
}
5656
void main() {
57-
D.new(1); // normal invocation
58-
const C.new(1); // const invocation
59-
var f = C.new; // tear-off
57+
D.new(1); // normal invocation.
58+
const C.new(1); // const invocation.
59+
new C.new(1); // explicit new invocation.
60+
var f = C.new; // tear-off.
6061
f(1);
6162
}
6263
```
6364

6465
Apart from the tear-off, this code will mean exactly the same thing as the same code without the `.new`. The tear-off cannot be performed without the `.new` because that expression already means something else.
6566

66-
The one thing we do *not* allow is `new C.new(1)`, in an explicit constructor invocation. That'd be too much of a good thing, and we are discouraging that use of `new`. We allow `const C.new(1)` because it means something else.
67-
6867
*With regard to tear-offs, <code>C.new</code> works exactly as if it had been a named constructor, with a corresponding constructor function named <code>C.new$tearoff</code>.*
6968

70-
We probably want to support `[C.new]` as a constructor link in DartDoc as well. In `dart:mirrors`, the name of the constructor is still just `C`, not `C.new` (that's not a valid symbol, and we don't want to break existing reflection using code).
69+
We probably want to support `[C.new]` as a constructor link in DartDoc as well. In `dart:mirrors`, the name of the constructor is still just `C`, not `C.new` (that's not a valid symbol, and we don't want to break existing reflection using code).
70+
71+
The grammar changes would affect the following productions by adding the `| \NEW{}` option to the name of the constructor, and a primary expression referencing the constructor by `.new`:
72+
73+
```ebnf
74+
<constructorInvocation> ::= \gnewline{}
75+
<typeName> <typeArguments> `.' (<identifier> | \NEW{}) <arguments>
76+
77+
<qualifiedName> ::= <typeIdentifier> `.' (<identifier> | \NEW{})
78+
\alt <typeIdentifier> `.' <typeIdentifier> `.' (<identifier> | \NEW{})
79+
80+
<constructorDesignation> ::= <typeIdentifier>
81+
\alt <qualifiedName>
82+
\alt <typeName> <typeArguments> (`.' (<identifier> | \NEW{}))?
83+
84+
<constructorName> ::= <typeIdentifier> (`.' (<identifier> | \NEW{}))?
85+
86+
<primary> ::=
87+
...
88+
\alt <typeIdentifier> (`.' <typeIdentifier>)? `.' '\NEW{}
89+
```
90+
91+
It would be a compile-time error to use the `.new` to denote anything which is not an "unnamed" constructor.
7192

7293
### Consequences
7394

7495
This proposal is deliberately non-breaking and backwards compatible.
7596

76-
It introduces new syntax for "unnamed" constructors, they are now `new`-named constructors, you can just omit the `new` in most cases, except tear-offs. This avoids conflicting with type literals when trying to tear off an unnamed constructor.
97+
It introduces new syntax for "unnamed" constructors, they are now "`new`-named" constructors, you can just omit the `new` in most cases except tear-offs. This avoids conflicting with type literals when trying to tear off an unnamed constructor.
7798

7899
We technically only need the `C.new` for tear-offs, and don't need to allow it in other places, but it would be (more) inconsistent to only allow the syntax in one place. Also, the same syntax may be useful for declaring and calling generic unnamed constructors in the future.
79100

80-
There is no easy way to abstract over the type parameters of the class. We could make `Set<T> Function<T>() makeSet = HashSet;` tear off as `<T>() => HashSet<T>()`, providing a generic function matching the generic class. *However*, we also want to introduce *generic constructors*, say `Map.fromIterable<T>(Iterable<T> elements, K key(T element), V value(T element))`, and tearing off that should create a generic function. Making generic tear-offs work with the *class* type parameters could interfere with this later feature. Not doing so is still a choice with consequence, we can't just allow it later if we change our minds. If `var makeFilled = List.filled;` is not generic now, it would be a breaking change to make it generic later. Adding `.new` syntax gives us a way to declare and invoke generic "unnamed" constructors, so it won't need to be `B<int><int>(…)`.
101+
We make the tear-off of the constructor of a generic class be a generic function. *However*, we also want to introduce *generic constructors*, say `Map.fromIterable<T>(Iterable<T> elements, K key(T element), V value(T element))`, and tearing off that should also create a generic function. Making generic tear-offs work with the *class* type parameters could interfere with this later feature. The most obvious solution is to combine class and constructor type parameters when tearing off a constructor, so the tear-off function of `Map.fromIterable` could become `Map<K, V> fromIterable$tearoff<K, V, T>(…)`. The issue with that is that *making* the constructor generic would be a breaking change, it changes the number of type parameters of the tear-off `Map.fromIterable`.
81102

82103
Constructors with very large argument lists will create very large function closures. Example:
83104

84105
```dart
85106
class C {
86107
final int? a, b, c, d, e, f, g, h, i, j, k, l, m;
87108
C({this.a, this.b, this.c, this.d, this.e, this.f, this.g, this.h, this.i, this.j, this.k, this.l, this.m});
109+
// Has constructor function:
110+
static C new$tearoff(
111+
int? a, int? b, int? c, int? d, int? e, int? f, int? g, int? h, int? i, int? j, int? k, int? l, int? m) =>
112+
C(a: a, b: b, c: c, d: d, e: e, f: f, g: g, h: h, j: j, l: l, m: m);
88113
}
89114
...
90-
void Function() f = C.new;
91-
// equivalent to:
92-
// = (int? a, int? b, int? c, int? d, int? e, int? f, int? g, int? h, int? i, int? j, int? k, int? l, int? m) =>
93-
// C(a: a, b: b, c: c, d: d, e: e, f: f, g: g, h: h, j: j, l: l, m: m);
115+
void Function() f = C.new; // closure of new$tearoff
94116
```
95117

96-
In this case, most of the parameters are *unnecessary*, and a tear-off expression of `() => C()` would likely be sufficient. However, that would prevent canonicalization, and would be inconsistent with what we do for function-tear-off. If the implementation is just a tear-off of an implicitly defined
118+
In this case, most of the parameters are *unnecessary*, and a tear-off expression of `() => C()` would likely be sufficient. That would prevent canonicalization, and would be inconsistent with what we do for function tear-off. If the implementation is just a tear-off of an implicitly defined
97119

98120
```dart
99121
static C new$tearOff(int? a, int? b, int? c, int? d, int? e, int? f, int? g, int? h, int? i, int? j, int? k, int? l, int? m) =>
100122
C(a: a, b: b, c: c, d: d, e: e, f: f, g: g, h: h, j: j, l: l, m: m);
101123
```
102124

103-
which can be tree-shaken if the constructor is never torn off, then the overhead should be fixed.
125+
which can be tree-shaken if the constructor is never torn off, then the overhead should be fixed. It will make it harder to tree-shake unused *parameters*, but no harder than for static functions which are also torn off.
104126

105127
## Possible Extensions
106128

107129
### Explicitly instantiated generics
108130

109-
We can only instantiate generic classes based on type inference. That means that `var makeIntList = List.filled;` won't work (it will have type `List<Object?> Function(int, Object?)`, you have to write out the entire type as `List<int> Function(int args, int value) = List.filled;`.
131+
We cannot control instantiation of generic constructor functions except using the context type.
132+
133+
That means that `var makeIntList = List.filled;` will have type `List<E> Function<E>(int, E)`, and to specialize it to integer lists, you have to write out the entire function type as `List<int> Function(int args, int value) = List.filled;`.
110134

111135
If we instead allow you to write `var makeIntList = List<int>.filled;`, then you would not need the context type.
112136

@@ -119,7 +143,18 @@ That is, we would allow the following expressions as well:
119143

120144
and in the semantics above, the type arguments to the corresponding constructor function uses the specified type arguments instead of inferring them.
121145

146+
Allowing explicit type arguments will require further language changes, possibly only changing the new `<primary>` production to:
147+
148+
```ebnf
149+
<primary> ::=
150+
...
151+
\alt <typeIdentifier> (`.' <typeIdentifier>)? <typeArguments>? `.' '\NEW{}
152+
```
153+
154+
We can go further and allow `C<typeArgs>` and `f<typeArgs>` as expressions by themselves, as an instantiated type `Type` object and an instantiated function tear-off. See issue [#123](https://github.com/dart-lang/language/issues/123). It's not *necessary* to allow `typeExpr<types>` as an expression for named constructor tear-off instantiation because there is always a <code>.*name*</code> or `.new` after the type.
155+
122156
## Versions
123157

124158
* 2.0: Initial version in this iteration. Proposed `new C` as unnamed tear-off syntax.
125159
* 2.1: Revision. Proposed `C.new` as unnamed tear-off syntax.
160+
* 2.2: Revision. Propose generic tear-off functions.

0 commit comments

Comments
 (0)