OLD | NEW |
---|---|
(Empty) | |
1 # Optional new/const | |
2 | |
3 Author: Lasse R.H. Nielsen ([lrn@google.com](mailto:lrn@google.com)) | |
4 | |
5 Version: 0.6 (2017-06-08) | |
6 | |
7 Status: Under discussion | |
8 | |
9 This informal specification documents a group of four related features. | |
10 | |
11 | |
12 ## Optional const/const insertion | |
13 | |
14 In current Dart code, some expressions are required to be compile-time constant expressions. | |
floitsch
2017/06/08 15:06:43
I would start this section differently:
In current
Lasse Reichstein Nielsen
2017/06/08 15:50:22
Acknowledged.
| |
15 | |
16 These are mainly sub-expressions of other compile-time constant expressions, but also the initializer expressions of const variables, default values for optiona l parameters, switch case expressions, and (sub-expressions of) metadata annotat ions. | |
floitsch
2017/06/08 15:06:43
These are mainly, -> "for example" ?
Lasse Reichstein Nielsen
2017/06/08 15:50:22
Acknowledged.
| |
17 | |
18 Whenever a map or list literal occurs as a sub-expression of a const expression, you have to write `const` again. This happens a lot in constant JSON-like struc tures. Example: | |
19 | |
20 | |
21 ``` | |
22 const dictionary = const { | |
23 "a": const ["able", "apple", "axis"], | |
24 "b": const ["banana", "bold", "burglary"], | |
25 … | |
26 }; | |
27 ``` | |
28 | |
29 | |
30 Here the `const` on the map and all the lists are *required*, which also means t hat they are *redundant* (and annoying to write). | |
31 | |
32 The "optional const" feature allows you to omit `const` where it is otherwise re quired. | |
33 | |
34 This can also be seen as an "automatic const insertion" feature that automatical ly inserts a `const` where it's necessary. | |
35 | |
36 The `const` prefix is used in front of map literals, list literals and *construc tor calls*. | |
37 | |
38 This feature allows it to be omitted from all of these is cases where it would b e required. That means we have un-prefixed constructor calls. | |
39 This is precedented in that metadata annotations can be written as `@Foo(constan tArg)`. | |
40 | |
41 It also intersects perfectly with the "optional new" feature below. | |
42 | |
43 The grammar needs to be extended to handle the case of `Foo<int>.bar()` which is currently not a valid expression (type parameters on methods only covers the un named constructor `Foo<int>()` case, not the named constructor invocation case). | |
44 | |
45 See http://dartbug.com/4046 and https://github.com/lrhn/dep-const/blob/master/pr oposal.md | |
46 | |
47 The syntax for a constructor call is less ambiguous than it used to be since gen eric methods have been added to the language, so `Bar(Foo<int, bar>(42))` has be en given a meaning consistent with a constructor call. | |
48 | |
49 | |
50 ### Informal specification | |
51 | |
52 | |
53 | |
54 * An expression occurs in a "const context" if it is | |
55 * a literal const `List`, const `Map` or const constructor invocation (`co nst {...}`, `const [...]`, `const Symbol(...)`, `@Symbol(...)`), | |
56 * a parameter default value, | |
57 * the initializer expression of a const variable, | |
58 * a case expression in a switch statement, or | |
59 * is a sub-expression of an expression in a const context. | |
floitsch
2017/06/08 15:06:43
Should this include the annotations?
Lasse Reichstein Nielsen
2017/06/08 15:50:22
It absolutely has to, and it does (`@Symbol(...)`
floitsch
2017/06/08 17:30:08
right.
| |
60 | |
61 That is: `const` introduces a const context for all its sub-expressions, as do the syntactic locations where only const expressions can occur. | |
62 | |
63 * If a non-const `List` literal, non-const `Map` literal or invocation express ion (including the new generic-class-member notation) occurs in a const context, it is equivalent to the same expression with a `const` in front. That is, you d on't have to write the `const` if it's required anyway. | |
64 * The grammar allows `type<typeArguments>(args)` and `type<typeArguments>.id(a rgs)` as an expression in a const context where it would accept the same thing p refixed by `const`. The latter would not otherwise be valid expressions in the c urrent grammar. | |
65 * Otherwise this is purely syntactic sugar, and existing implementations can h andle this at the syntax level, by inserting a synthetic `const`. | |
66 | |
67 | |
68 ## Optional new/new insertion | |
69 | |
70 Currently, a call to a constructor without a prefixed `new` (or `const`) is inva lid. | |
71 | |
72 So, if the class `Foo` has a constructor `bar` then `Foo.bar()` will be a static warning/runtime failure (and strong mode compile-time error). | |
73 | |
74 Instead we now specify such an expression to be equivalent to `new Foo.bar()` (e xcept in a const context where it's equivalent to `const Foo.bar()` due to the o ptional `const` introduced above). | |
75 | |
76 The "empty-named" constructor also works this way: `Foo()` is currently a runtim e-error, so we can change its meaning to be equivalent to `new Foo()`. | |
77 | |
78 Like for optional const, we need to extend the grammar to accept `List<int>.fill ed(4, 42)`. | |
79 | |
80 The `new` is optional, not prohibited. It may still be useful to write `new` as documentation that this actually creates a new object. Also, some constructor na mes might be less readable without the `new` in front. | |
81 | |
82 In the longer run, we may want to remove `new` so there isn't two ways to do the same thing. | |
83 | |
84 Having optional `new` means that changing a static method to be a constructor is not a breaking change. Since it's only optional, not disallowed, changing in th e other direction is a breaking change. | |
85 | |
86 See: http://dartbug.com/5680, http://dartbug.com/18241, http://dartbug.com/20750 . | |
87 | |
floitsch
2017/06/08 15:06:43
One thing that needs to be mentioned:
```
var x =
Lasse Reichstein Nielsen
2017/06/08 15:50:22
Ah, you are thinking of the "implicit const when c
floitsch
2017/06/08 17:30:08
It's independent, but if we make `new` optional, t
Lasse Reichstein Nielsen
2017/06/19 12:15:36
From this proposal, it would add `new` in front of
| |
88 | |
89 ### Informal specification | |
90 | |
91 * An expression on one of the forms: | |
92 * `Foo(args)` | |
93 * `Foo<types>(args)` | |
94 * `Foo.bar(args)` | |
95 * `Foo<types>.bar(args)` | |
96 * `prefix.Foo(args)` | |
97 * `prefix.Foo<types>(args)` | |
98 * `prefix.Foo.bar(args)` | |
99 * `prefix.Foo<types>.bar(args)` | |
100 | |
101 where `Foo`/`prefix.Foo` denotes a class and `bar` is a named constructor of the class, and that is not in a const context, are no longer errors. | |
102 | |
103 * Instead they are equivalent to the same expression with a `new` in front. Th is makes the `new` optional, but still allowed. | |
104 * The grammar allows `type<typeArguments>(args)` and `type<typeArguments>.id(a rgs)` as an expression where it would allow the same thing prefixed by `new`. Th e latter would not otherwise be valid syntax in the current grammar. | |
105 * Otherwise this is purely syntactic sugar, and existing implementations can h andle this at the syntax level, by inserting a synthetic `new` in front of non-c onst expressions that would otherwise try to invoke a constructor, which is stat ically detectable. | |
106 | |
107 | |
108 ## Optional new/const in *potentially* const expressions | |
109 | |
110 Together, the "optional const" and "optional new" features describe what happens if you omit the operator on a constructor call in a const or normal expression. However, there is one more kind of expression in Dart - the *potentially consta nt expression*, which only occurs in the initializer list of a generative const constructor. | |
111 | |
112 Potentially constant expressions have the problem that you can't write `new Foo( x)` in them, because that expression is never constant, and you can't write `con st Foo(x)` if `x` is a parameter, because `x` isn't always constant. The same pr oblem applies to list and map literals. | |
113 | |
114 Allowing you to omit the `new`/`const`, and just write nothing, gives us a way t o provide a new meaning to a constructor invocation (and list and map literals) in a potentially const expression: Treat it as `const` when invoked as a const c onstructor, and as `new` when invoking normally. | |
115 | |
116 This also allows you to use the *type parameters* of the constructor to create n ew objects, like `class Foo<T> { final List<T> list; const Foo(int length) : lis t = List<T>(length); }`. Basically, it can treat the type parameter as a potenti ally constant variable as well, and use it.. | |
117 | |
118 The sub-expressions must still all be potentially const, but that's not a big pr oblem. | |
119 | |
120 There is another problem that is harder to handle - avoiding infinite recursion at compile-time. | |
121 | |
122 If a constructor can call another constructor as a potentially constant expressi on, then it's possible to recurse deeply - or infinitely. | |
123 | |
124 Example: | |
125 | |
126 | |
127 ```dart | |
128 class C { | |
129 final int value; | |
130 final C left; | |
131 final C right; | |
132 const C(int start, int depth) | |
133 : left = (depth == 0 ? null : C(start, depth - 1)), | |
134 value = start + (1 << depth), | |
135 right = (depth == 0 ? null : C(start + (1 << depth), depth - 1)); | |
136 } | |
137 ``` | |
138 | |
139 | |
140 This class would be able to generate a complete binary tree of any depth as a co mpile-time constant, using only *potentially constant* expressions and `const`/` new`-less constructor calls. | |
141 | |
142 It's very hard to distinguish this case from one that recurses infinitely, and t he latter needs to be able to be caught and rejected at compile-time. We need to add some cycle-detection to the semantics to prevent arbitrary recursion. Since no recursion is currently possible, it won't break anything. | |
143 | |
144 Proposed restriction: Don't allow a constant constructor invocation to invoke th e same constructor again *directly*, where "directly" means: | |
145 | |
146 | |
147 | |
148 * as a sub-expression of an expression in the initializer list, or | |
149 * *directly* in the initializer list of another const constructor that is invo ked by a sub-expression in the initializer list. | |
150 | |
151 This transitively prevents the constructor to be called again without any limiti ng constraint. | |
152 | |
153 It does not prevent the invocation from referring to a const variable whose valu e was created using the same constructor, so the following is allowed: | |
154 | |
155 | |
156 ```dart | |
157 const c0 = const C(0); | |
158 const c43 = const C(43); | |
159 class C { | |
160 final v; | |
161 const C(x) : v = ((x % 2 == 0) ? x : c0); // Silly but valid. | |
162 } | |
163 ``` | |
164 | |
165 | |
166 The `const C(0)` invocation does not invoke `C` again, and the `const C(43)` inv ocation doesn't invoke `C` again, it just refers to another (already created) co nst value. | |
167 | |
168 As usual, a const *variable* cannot refer to itself when its value is evaluated. | |
169 | |
170 This restriction avoids infinite regress because the number of const variables a re at most linear in the source code of the program while still allowing some re ference to values of the same type. | |
171 | |
172 Breaking the recursive constraint at variables also has the advantage that a con st variable can be represented only by its value. It doesn't need to remember wh ich constructors were used to create that value, just to be able to give an erro r in cases where that constructor refers back to the variable. | |
173 | |
174 See: [issue 18241](http://dartbug.com/18241) | |
175 | |
176 | |
177 ### Informal specification | |
178 | |
179 In short: | |
180 | |
181 * A const constructor introduces a "potentially const context" for its initial izer list. | |
182 * This is treated similarly to a const context when the constructor is invoked in a const expression and as normal expression when the constructor is invoked as a non-const expression., | |
183 * This means that `const` can be omitted in front of `List` literals, `Map` li terals and constructor invocations. | |
184 * All subexpressions of such expressions must still be *potentially const expr essions*, otherwise it's still an error. | |
185 * It is a compile-time error if a const constructor invoked in a const express ion causes itself to be invoked again *directly* (immediately in the initializer list or recursively while evaluating another const constructor invocation). It' s not a problem to refer to a const variable that is created using the same cons tructor. (This is different from what the VM currently does - the analyzer doesn 't detect cycles, and dart2js stack-overflows). | |
186 * The grammar allows `type<typeArguments>(args)` and `type<typeArguments>.foo( args)` as an expression in potentially const contexts, where the latter isn't cu rrently valid syntax, and the former wouldn't be allowed in a const constructor. | |
187 * This is not just syntactic sugar: | |
188 * It makes const and non-const constructor invocations differ in behavior. This alone can be simulated by treating it as two different constructors (perha ps even rewriting it into two constructors, and change invocations to pick the c orrect one based on context). | |
189 * The const version of the constructor now allows parameters, including ty pe parameters, to occur as arguments to constructor calls and as list/map member s. This is completely new. | |
190 * The language still satisfies that there is only one compile-time constan t value associated with each `const` expression, but some expression in const co nstructor initializer lists are no longer const expressions, they are just used as part of creating (potentially nested) const values for the const expressions. Effectively the recursive constructor calls need to be unfolded at each creatio n point, not just the first level. Each such unfolding is guaranteed to be finit e because it can't call the same constructor recursively and it stops at const v ariable references (or literals). It *can* have size exponential in the code siz e, though. | |
191 | |
192 | |
193 ## Constructor tear-offs | |
194 | |
195 With constructors being callable like normal static functions, it makes sense to also allow them to be *torn off* in the same way too. That means that if `Foo.b ar` is a constructor of class `Foo`, then the *expression* `Foo.bar` will be a t ear-off of the constructor (it evaluates to a function with the same signature a s the constructor, and calling the function will invoke the constructor with the same arguments and return the result). | |
196 | |
197 The tear-off of a constructor from a non-generic class is treated like a tear-of f of a static method - it's a compile-time constant expression and it is canonic alized. A generic class constructor tear-off is treated like the tear-off of an instance method. It is not a compile-time constant and it isn't required to be c anonicalized, but it must still be *equal* to the same constructor torn off the same class instantiated with the same type parameters. | |
198 | |
199 For a non-named constructor, the expression `Foo` already has a meaning – it eva luates to the `Type` object for the class `Foo` – so we can't use that to refer to the unnamed constructor. | |
200 | |
201 Instead we introduce the notation `Foo.new`. This is otherwise a syntax error, s o it doesn't conflict with anything. | |
202 | |
203 For named constructors, an expression like `Foo<int>.bar` (not followed by argum ents like the cases above) is not allowed by the syntax, so there is no conflict . | |
204 | |
205 This tear-off syntax is something we want in any case, independently of the opti onal new/const/potential const changes above. With this tear-off syntax, you can also call a named constructor without `new` just by adding parentheses: `(Foo.b ar)(42)` instead of `new Foo.bar(42)`, or even `Foo.new(42)` instead of `new Foo (42)` (if that's the kind of thing you like). That makes requiring `new` less in teresting since it is already not *required*, so adding the tear-off syntax and not the optional `new`, is not really a reasonable combination. | |
206 | |
207 | |
208 ### Informal specification | |
209 | |
210 * An expression *x* on one of the forms: | |
211 * `Foo.new` | |
212 * `Foo<types>.new` | |
213 * `Foo.bar` | |
214 * `Foo<types>.bar` | |
215 * `prefix.Foo.new` | |
216 * `prefix.Foo<types>.new` | |
217 * `prefix.Foo.bar` | |
218 * `prefix.Foo<types>.bar` | |
219 where `Foo` and `prefix.Foo` denotes a class and `bar` is a constructor of `Foo` , and the expression is not followed by arguments `(args)`, is no longer an erro r. | |
floitsch
2017/06/08 15:06:43
Not yet for this document, but we might need to up
Lasse Reichstein Nielsen
2017/06/08 15:50:22
Can we do a specializing tear-off of those?
That i
floitsch
2017/06/08 17:30:08
We should be able to do that.
We also want to auto
Lasse Reichstein Nielsen
2017/06/19 12:15:36
I'm not writing anything about that for now. If we
| |
220 | |
221 Not included are expressions like `Foo..new(x)` or `Foo..bar(x)`. This is actual ly an argument against adding static cascades (`C..foo()..bar()` isn't currently a static call, it's a cascade on the `Type` object). | |
222 | |
223 * Instead of being an error, the expression evaluates to a function value | |
224 * with the same signature as the constructor (same parameters, default val ues, and `Foo` or `Foo<types>` as return type), | |
225 * which, when called with `args`, returns the same result as `new x'(args) ` where `x'` is `x` without any `.new`. | |
226 * if `Foo` is not generic, the expression is a canonicalized compile-time constant (like a static method). | |
227 * If `Foo` is generic, the function is `==` to another tear off of the sam e constructor from "the same instantiation" of the class (like an instance metho d tear-off). We have to nail down what "the same instantiation" means, especiall y if `void == Object` in our type system. | |
228 * This can be implemented by adding a static method for each non-generic class constructor: | |
229 | |
230 ```dart | |
231 class C { | |
232 C(x1, …, xn) : … { body } | |
233 static C C_asFunction(x1, …, xn) => new C(x1,...xn); | |
234 } | |
235 ``` | |
236 | |
237 The tear-off of `C.new` is just `C_asFunction`. | |
238 | |
239 * … and adding a new helper class for each generic class with constructors: | |
240 | |
241 ```dart | |
242 class D<T> { | |
243 D(x1, …, xn) : … { body } | |
244 } | |
245 class D_constructors<T> { | |
246 const D_constructors(); | |
247 D_asFunction(x1, …, xn) => new D<T>(x1, …, xn); | |
248 } | |
249 ``` | |
250 | |
251 Then the tear-off of `D<T>.new` is `const D_constructors<T>().D_asFunction`. If the type is a non-const type parameter, the equality is harder to preserve, and the implementation might need to cache and canonicalize the `D_constructors` instances that it does the method tear-offs from. | |
252 | |
253 * In strong mode, method type parameters are not erased, so the implementation might be able to just create a closure containing the type parameter without a helper class (but equality might be harder to get correct that way). | |
254 * In most cases, implementations should be able to be more efficient than this rewriting if they can refer directly to their representation of the constructor . | |
255 | |
256 | |
257 ## Migration | |
258 | |
259 All the changes in this document are non-breaking - they assign meaning to synta x that was previously an error, either statically or dynamically. As such, code does not *need* to be migrated. | |
260 | |
261 We maybe want to migrate library code so that it can serve as a good example. It 's not as important as features that affect the actual API. The most visible cha nge will likely be that some constructors can now be torn off as a const express ion and used as a parameter default value. | |
262 | |
263 All other uses will occur inside method bodies or initializer expressions. | |
264 | |
265 Removing `new` is easy, and can be done by a simple RegExp replace. | |
266 | |
267 Removing nested `const` probably needs manual attention ("nested" isn't a regula r property). | |
268 | |
269 Using constructor tear-offs will likely be the most visible change, with cases l ike: | |
270 | |
271 | |
272 ```dart | |
273 map.putIfAbsent(42, List<int>.new); // Rather than map.putIfAbsent(42, () => <i nt>[]) | |
floitsch
2017/06/08 15:06:43
Wouldn't put that example. I don't think it reads
Lasse Reichstein Nielsen
2017/06/08 15:50:22
Acknowledged.
Lasse Reichstein Nielsen
2017/06/19 12:15:37
But I don't have a better example, so suggestions
| |
274 bars.map(Foo.fromBar)... // rather than bars.map((x) => new Foo.fromBar(x)) | |
275 ``` | |
276 | |
277 | |
278 Once the features are implemented, this can be either done once and for all, or incrementally since each change is independent. | |
279 | |
280 | |
281 ## Related possible features | |
282 | |
283 | |
284 ### Type variables in static methods | |
285 | |
286 When you invoke a static method, you use the class name as a name-space, e.g., ` Foo.bar()`. | |
287 | |
288 If `Foo` is a generic class, you are not allowed to write `Foo<int>.bar()`. Howe ver, that notation is necessary for optional `new`/`const` anyway, so we might c onsider allowing it in general. The meaning is simple: the type parameters of a surrounding class will be in scope for static methods, and can be used both in t he signature and the body of the static functions. | |
289 | |
290 If the type parameter is omitted, it defaults to dynamic/is inferred to somethin g, and it can be captured by the `Foo<int>.bar` tear-off. | |
291 | |
292 This is in agreement with the language specification that generally treats `List <int>` as a class and the generic `List` class declaration as declaring a mappin g from type arguments to classes. | |
293 | |
294 It makes constructors and static methods more symmetric. | |
295 | |
296 It's not entirely without cost - a static method on a class with a bound can onl y be used if you can properly instantiate the type parameter with something sati sfying the bound. A class like | |
297 | |
298 | |
299 ```dart | |
300 class C<T extends C<T>> { | |
301 int compare(T other); | |
302 static int compareAny(dynamic o1, dynamic o2) => o1.compare(o2); | |
303 } | |
304 ``` | |
305 | |
306 | |
307 would not be usable as `C.compareAny(v1, v2)` because `C` cannot be automaticall y instantiated to a valid bound. That is a regression compared to now, where any static method can be called on any class without concern for the type bound. Th is regression might be reason enough to drop this feature. | |
308 | |
309 Also, if the class type parameters are visible in members, including getters and setters, it should mean that that *static fields* would have to exist for each instantiation, not just once. That's so incompatible with the current behavior, and most likely completely unexpected to users. This idea is unlikely to ever ha ppen. | |
floitsch
2017/06/08 15:06:43
Was that all we had against it? There weren't othe
Lasse Reichstein Nielsen
2017/06/08 15:50:22
I don't remember any other big issues (but I could
floitsch
2017/06/08 17:30:08
Rereading it now, that is actually enough.
Having
Lasse Reichstein Nielsen
2017/06/19 12:15:37
Done.
| |
310 | |
311 | |
312 ### Instantiated Type objects | |
313 | |
314 The changes in this document allows `Foo<T>` to occur: | |
315 | |
316 * Followed by arguments, `Foo<T>(args)` | |
317 * Followed by an identifier, `Foo<T>.bar` (and optionally arguments). | |
318 * Followed by `new`, `Foo<T>.new`. | |
319 | |
320 but doesn't allow `Foo<T>` by itself, not even for the non-named constructor. | |
321 | |
322 The syntax is available, and needs to be recognized in most settings anyway, so we could allow it as a type literal expression. That would allow the expression `List<int>` to evaluate to the *Type* object for the class *List<int>*. It's bee n a long time (refused) request: [issue 23221](http://dartbug.com/23221). | |
323 | |
324 | |
325 ### Revisions | |
326 | |
327 0.5 (2017-02-24) Initial version. | |
328 | |
329 0.6 (2017-06-08) Added "Migration" section, minor tweaks. | |
OLD | NEW |