You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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).
6
6
7
7
It's an annoying shortcoming. We want to allow constructors to be torn off, and users have requested it repeatedly ([#216](https://github.com/dart-lang/language/issues/216), [#1429](https://github.com/dart-lang/language/issues/1429)).
8
8
9
-
This is a proposal for constructor tear-offs which introduces minimal new syntax, but enables all constructors to be torn off, and with some discussion on possible extensions to the syntax.
9
+
This is a proposal for constructor tear-offs which introduces new syntax for the unnamed constructor, for constructor tear-offs, and for explicit instantiation of generic functions tear-offs in general.
10
10
11
11
## Goal
12
12
@@ -25,7 +25,7 @@ var v5 = (C.name)<typeArgs>(args);
25
25
26
26
should always give equivalent values for `v1` and `v2`, and for `v3`, `v4` and `v5`.
27
27
28
-
We also want a consistent and useful *identity*and *equality*of the torn off functions, with the tear-off expression being a constant expression where possible. It should match what we already do for static functiontear-off where that makes sense.
28
+
We also want a consistent and useful *identity*and *equality*of the torn off functions, with the tear-off expression being a constant expression where possible. It should match what we already do for static functiontear-off where that makes sense.
29
29
30
30
## Proposal
31
31
@@ -40,13 +40,13 @@ If *C* denotes a class declaration and *C.name* is the name of a constructor of
40
40
41
41
just as you can currently invoke the constructor as <code>*C*.*name*(*args*)</code>, or <code>*C*\<*typeArgs*>.*name*(*args*)</code>.
42
42
43
-
_The former syntax, without type arguments, is currently allowed by the language grammar, but is rejected by the static semantics as not being a valid expression. The latter syntax is not currently grammatically an expression. Both can occur as part of a constructor invocation, but cannot be expressions by themselves because they have no values. We introduce a static and dynamic expression semantic for such a *named constructor tear-off expression*._
43
+
_The former syntax, without type arguments, is currently allowed by the language grammar, but is rejected by the static semantics as not being a valid expression. The latter syntax is not currently grammatically an expression. Both can occur as part of a_ constructor invocation_, but cannot be expressions by themselves because they have no values. We introduce a static and dynamic expression semantic for such a *named constructor tear-off expression*._
44
44
45
-
A named constructor tear-off expression of one of the forms above 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*,with a fresh name here represented by adding `$tearoff`:
45
+
A named constructor tear-off expression of one of the forms above 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*,with a fresh name here represented by adding `$tearoff`:
If *C*is not generic, then <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*(including bounds), and <code>\<*typeArgs*></code> applies those type parameter variables directly as type arguments to *C*.
49
+
If *C*is not generic, then <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*(including bounds), and <code>\<*typeArgs*></code> applies those type parameter variables directly as type arguments to *C*.
50
50
51
51
Similarly, <code>*params*</code> is *almost* exactly the same parameter list as the constructor *C*.*name*, with the one exception that *initializing formals* are represented by normal parameters with the same name and 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.
52
52
@@ -71,15 +71,17 @@ The constant-ness, identity and equality of the torn-off constructor functions b
71
71
72
72
The static type of the named constructor tear-off expression is the same as the static type of the corresponding constructor function tear-off.
73
73
74
+
This introduces an **ambiguity** in the grammar. If `List.filled` is a valid expression, then `List.filled(4, 4)` can both be a constructor invocation *and* a tear-off followed by a function invocation. We only allow the constructor invocation when it's not followed by a *typeArguments* or *arguments* production (or, possibly, when it's not followed by a `<` or `(` character). _We don't want to allow `List.filled<int>` to be interpreted as `(List.filled)<int>`. Just write the `List<int>.filled` to begin with!_
75
+
74
76
#### Tearing off constructors from type aliases
75
77
76
78
*With generalized type-aliases*, it's possible to declare a class-alias like `typedef IntList = List<int>;`. We allow calling constructors on such a type alias, so we will also allow tearing off such a constructor.
77
79
78
-
In general, a *non-generic* type alias is just expanded to its aliased type, then the tear-off happens on that type. Tearing off `IntList.filled` will act like tearing off `List<int>.filled`, it automatically instantiates the class type parameter to the specified type. It's constant and canonicalized to the same function as `List<int>.filled`.
80
+
In general, a *non-generic* type alias is just expanded to its aliased type, then the tear-off happens on that type. Tearing off `IntList.filled` will act like tearing off `List<int>.filled`, it automatically instantiates the class type parameter to the specified type. It's constant and canonicalized to the same function as `List<int>.filled`._In other words, the alias is treated as an actual alias for the type it aliases._
79
81
80
82
This differs for a *generic* type alias. If the type alias is *instantiated* (implicitly or explicitly), then the result is still the same as tearing off the aliased type directly, and it's constant and canonicalized if the type arguments are constant.
81
83
82
-
If the type alias is *not* instantiated, tearing off a constructor works equivalently to tearing off a corresponding generic function where the generics match the *type alias*, not the underlying class. The result is a compile-time constant.
84
+
If the type alias is *not* instantiated, then it's a function from types to types, not an alias for a single type, and tearing off a constructor works equivalently to tearing off a corresponding generic function where the generics match the *type alias*, not the underlying class. The result is a compile-time constant.
@@ -139,7 +142,7 @@ We probably want to support `[C.new]` as a constructor link in DartDoc as well.
139
142
140
143
The grammar will be changed to allow ``<identifier> |`new'`` anywhere we currently denote a named constructor name, and we make it a primary expression to tear-off an unnamed constructor as `classRef.new`.
141
144
142
-
### Instantiated classes and function tear-offs
145
+
### Explicitly instantiated classes and function tear-offs
143
146
144
147
The above allows you to explicitly instantiate a constructor tear-off as `List<int>.filled`. We do not have a similar ability to explicitly instantiate function tear-offs. Currently you have to provide a context type and rely on implicit instantiation if you want to tear off an instantiated version of a generic function.
145
148
@@ -172,19 +175,128 @@ The static type of the explicitly instantiated tear-offs are the same as if the
172
175
173
176
The static type of the instantiated type literal is `Type`. This also satisfies issue [#123](https://github.com/dart-lang/language/issues/123).
174
177
178
+
We **do not allow***dynamic* explicit instantiation. If an expression `e` has type `dynamic` (or `Never`), then `e.foo<int>` is a compile-time error for any name `foo`. (It'd be valid for a member of `Object` if it was a generic functions, but none of the are). It's not possible to do implicit instantiation without knowing the member signature. _(Alternative: Allow it, and handle it all at run-time, including any errors from having the wrong number or types of arguments, or there not being an instantiable `foo` member.)_
179
+
180
+
This introduces **new ambiguities** in the grammar, similar to the one we introduced with generic functions. Examples include:
181
+
182
+
```dart
183
+
f(a<b,c>(d)); // Existing ambiguity, resolved to a generic method call.
The `x.a<b,c>` can be an explicitly instantiated generic constructor (or function) tear-off or an explicitly instantiated type literal named using a prefix, which is new. While neither type objects nor functions declare `operator-` or `operator[]`, such could be added using extension methods.
189
+
190
+
We will disambiguate such situations *heuristically* based on the token following the `>`. In the existing ambigurity we treat `(` as a sign that it's a generic invocation. If the next character is one which *cannot* start a new expression (and be the operand of a `>` operator), the prior tokens is parsed as an explicit instantiation. If the token *can* start a new expression, then we make a choice depending on what we consider the most likely intention (that's specifically `-` and `[` in the examples above).
191
+
192
+
The look-ahead tokens which force the prior tokens to be type arguments are:
Any other token following the ambiguous `>` will make the prior tokens be parsed as a comma separated `<` and `>` operator invocations.
199
+
200
+
_We could add `&&` and `||` to the list, but it won't matter since the result is going to be invalid in either case._
201
+
202
+
_This might set us up for problems if we ever decide to use any of the infix operators as prefix operators, like `-`, but it does allow defining those operators on `Type` or `Function` and using them. Not allowing the infix operators is an alternative_
203
+
204
+
**Identity and equality** is not affected by explicit instantiation, it works exactly like if the same types had been inferred.
205
+
206
+
### No instantiated tearing off function `call` methods
207
+
208
+
We further formalize a restriction that the current implementation has.
209
+
210
+
Currently you can do instantiated tear-offs of *instance* methods. We restrict that to *interface* methods, which precisely excludes the `call` methods of function types. We do not allow instantiating function *values*, and therefore also do not allow side-stepping that restriction by instantiation the `.call` "instance" method of such a value.
211
+
212
+
That makes it a compile-time error to *explicitly* instantiate the `call` method of an expression with a function type or of type `Function`, and the tear-off of a `call` method of a function type is not subject to implicit instantiation (so the tear-off is always generic, even if the context type requires it not to be).
213
+
175
214
### Grammar changes
176
215
177
216
The grammar changes necessary for these changes will be provided in a separate document.
178
217
218
+
## Summary
219
+
220
+
We allow `TypeName.name` and `TypeName<typeArgs>.name`, when not followed by a type argument list or function argument list, as expressions which creates tear-offs of the the constructor `TypeName.name`. The `TypeName` can refer to a class declaration or to a type alias declaration which aliases a class.
We allow `TypeName.new` and `TypeName<typeArgs>.new` everywhere we allow a reference to a named constructor. It instead refers to the unnamed constructor. We allow tear-offs of the unnamed constructor by using `.new` and then treating it as a named constructor tear-off. Examples:
231
+
232
+
```dart
233
+
class C<T> {
234
+
final T x;
235
+
const C.new(this.x); // Same as: `const C(this.x);`
236
+
C.other(T x) : this.new(x); // Same as: `: this(x)`
237
+
factory C.d(int x) = D<T>.new; // same as: `= D<T>;`
238
+
}
239
+
class D<T> extends C<T> {
240
+
const D(T x) : super.new(x); // Same as: `: super(x);`
const C<num>.new(0); // Same as: `const C<num>(0);`.
245
+
new C.new(0); // Same as `new C(0);`.
246
+
new C<num>.new(0); // Same as `new C<num>(0);`.
247
+
C.new(0); // Same as `C(0);`.
248
+
C<num>.new(0); // Same as `C<num>(0);`.
249
+
var f1 = C.new; // New tear-off, not expressible without `.new`.
250
+
var f2 = C<num>.new; // New tear-off, not expressible without `.new`.
251
+
}
252
+
```
253
+
254
+
We allow *explicit instantiation* of tear-offs and type literals, by allowing type arguments where we would otherwise do implicit instantiation of tear-offs, or after type literals. Examples:
255
+
256
+
```dart
257
+
typedef ListList<T> = List<List<T>>;
258
+
T top<T>(T value) => value;
259
+
class C {
260
+
static T stat<T>(T value) => value;
261
+
T inst<T>(T value) => value;
262
+
}
263
+
void main() {
264
+
// Type literals.
265
+
var t1 = List<int>; // Type object for `List<int>`.
266
+
var t2 = ListList<int>; // Type object for `List<List<int>>`.
267
+
// Tear-offs.
268
+
T local<T>(T value) => value;
269
+
270
+
const f1 = top<int>; // int Function(int), works like (int $) => top<int>($);
271
+
const f2 = C.stat<int>; // int Function(int), works like (int $) => C.stat<int>($);
272
+
var c = C();
273
+
var f3 = C().inst<int>; // int Function(int), works like (int $) => c.inst<int>($);
274
+
var f4 = local<int>; // int Function(int), works like (int $) => local<int>($);
275
+
276
+
var typeName = List<int>.toString();
277
+
var functionTypeName = local<int>.runtimeType.toString();
278
+
}
279
+
```
280
+
281
+
Finally, we formalize the current behavior disallowing instantiated tear-off of `call` methods of function-typed values.
282
+
283
+
```dart
284
+
T func<T>(T value) => value;
285
+
var funcValue = func;
286
+
int Function(int) f = funcValue.call; // Disallow!
287
+
```
288
+
289
+
We can detect these statically, and they always throw at run-time, so we can special case them.
290
+
179
291
### Consequences
180
292
181
-
This proposal is non-breaking and backwards compatible.
293
+
This proposal is non-breaking and backwards compatible. Where we introduce new syntactic ambiguities, we retain the current interpretation.
182
294
183
295
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.
184
296
185
297
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.
186
298
187
-
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`,whereas making a previously non-generic function into a generic one is not breaking for existing invocations (they'll infer the type arguments).
299
+
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`,whereas making a previously non-generic function into a generic one is not breaking for existing invocations (they'll infer the type arguments).
188
300
189
301
Constructors with very large argument lists will create very large function closures. Example:
190
302
@@ -209,3 +321,4 @@ In this case, most of the parameters are *unnecessary*, and a tear-off expressio
209
321
* 2.1: Revision. Proposed `C.new` as unnamed tear-off syntax.
0 commit comments