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.7 (2017-06-19) | |
6 | |
7 Status: Under discussion | |
8 | |
9 This informal specification documents a group of four related features. | |
10 * Optional `const` | |
11 * Optional `new` | |
12 * Constructor tear-offs | |
13 * Potentially constant auto-`new`/`const`. | |
14 | |
15 These are ordered roughly in order of priority and complexity. The constructor t ear-offs feature effectively subsumes and extends the optional `new` feature. | |
16 | |
17 ## Optional const (aka. "const insertion") | |
18 | |
19 In current Dart code, every compile-time constant expression (except for annotat ions) must be prefixed with a `const` keyword. This is the case, even when the c ontext requires the expression to be a compile-time constant expression. | |
20 | |
21 For example, inside a `const` list or map, all elements must be compile-time con stants. This leads to repeated `const` keywords in nested expressions: | |
22 | |
23 ```dart | |
24 const dictionary = const { | |
25 "a": const ["able", "apple", "axis"], | |
26 "b": const ["banana", "bold", "burglary"], | |
27 … | |
28 }; | |
29 ``` | |
30 | |
31 Here the `const` on the map and all the lists are *required*, which also means t hat they are *redundant* (and annoying to have to write). | |
32 | |
33 The "optional const" feature allows you to omit the `const` prefix in places whe re it would otherwise be required. It is effectively optional. | |
34 | |
35 The feature can also be seen as an "automatic const insertion" feature that auto matically inserts the missing `const` where it's necessary. The end effect is th e same - the user can omit writing the redundant `const`. | |
36 This is somewhat precedented in that metadata annotations can be written as `@Fo o(constantArg)`. | |
37 | |
38 Making `const` optional intersects perfectly with the "optional new" feature bel ow, which does the same thing for `new`. | |
39 | |
40 Currently, the `const` prefix is used in front of map literals, list literals an d *constructor calls*. | |
41 Omitting the `const` prefix from list and map literals does not introduce a need for new syntax, it's just the plain list and map literal syntax. | |
floitsch
2017/06/19 13:33:48
for new syntax, since that syntax is already used
Lasse Reichstein Nielsen
2017/06/20 11:18:43
Done.
| |
42 That doesn't apply to un-prefixed constructor calls – those do introduce a synta x that isn't currently allowed: | |
43 `MyClass<SomeType>.name(arg)`. The language allows generic function invocation, which covers the unnamed constructor call `MyClass<SomeType>(arg)`, but it doesn 't allow applying type parameters to an identifier and *not* immediately calling the result. | |
44 | |
45 To allow all const constructor invocations to omit the `const`, the grammar need s to be extended to handle the case of `Nyclass<SomeType>.name(arg)`. | |
floitsch
2017/06/19 13:33:48
Myclass ?
Lasse Reichstein Nielsen
2017/06/20 11:18:43
Done.
| |
46 This syntax will only apply to unprefixed constructor invocations (at least unle ss we also introduce type-instantiated generic method tear-offs). | |
47 | |
48 ### Prior discussion | |
49 See http://dartbug.com/4046 and https://github.com/lrhn/dep-const/blob/master/pr oposal.md | |
50 | |
51 The syntax for a constructor call is less ambiguous now than when these proposal s were drafted, because generic methods have since been added to the language. T he language has already decided how to resolve parsing of the otherwise ambiguou s `Bar(Foo<int, bar>(42))`. | |
52 | |
53 | |
54 ### Informal specification | |
55 | |
56 * An expression occurs in a "const context" if it is | |
57 * a literal const `List`, const `Map` or const constructor invocation (`co nst {...}`, `const [...]`, `const Symbol(...)`, `@Symbol(...)`), | |
58 * a parameter default value, | |
59 * the initializer expression of a const variable, | |
60 * a case expression in a switch statement, or | |
61 * is a sub-expression of an expression in a const context. | |
62 | |
63 That is: `const` introduces a const context for all its sub-expressions, as do the syntactic locations where only const expressions can occur. | |
64 | |
65 * 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. | |
66 That is, an expression on one of the forms: | |
67 * `Foo(args)` | |
68 * `Foo<types>(args)` | |
69 * `Foo.bar(args)` | |
70 * `Foo<types>.bar(args)` | |
71 * `prefix.Foo(args)` | |
72 * `prefix.Foo<types>(args)` | |
73 * `prefix.Foo.bar(args)` | |
74 * `prefix.Foo<types>.bar(args)` | |
75 * `[elements]` | |
76 * `{mapping}` | |
77 | |
78 becomes valid in a `const` context. | |
79 | |
80 * The grammar is extended to allow `Foo<types>.id(args)` and `prefix.Foo<typeA rguments>.id(args)` as an expression. They would not otherwise be valid expressi ons anywhere in the current grammar. They still only work in a const context (it 's a compile-time error if they occur elsewhere, just not a grammatical error). | |
81 | |
82 * Otherwise this is purely syntactic sugar, and existing implementations can h andle this at the syntactic level by inserting the appropriate synthetic `const` prefixes. | |
83 | |
84 | |
85 ## Optional new (aka. "new insertion") | |
86 | |
87 Currently, a call to a constructor without a prefixed `new` (or `const`) is inva lid. With the optional const feature above, it would become valid in a const con text, but not outside of a const context. | |
88 | |
89 So, if the class `Foo` has a constructor `bar` then `Foo.bar()` is currently a s tatic warning/runtime failure (and strong mode compile-time error). | |
90 | |
91 Like for "optional const", we now specify such an expression to be equivalent to `new Foo.bar()` (except in a const context where it's still equivalent to `cons t Foo.bar()`). | |
92 | |
93 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()`. | |
94 | |
95 Like for optional const, we need to extend the grammar to accept `List<int>.fill ed(4, 42)`. | |
96 | |
97 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. | |
98 | |
99 In the longer run, we may want to remove `new` so there won't be two ways to do the same thing, but whether that is viable depends on choices about other featur es that we are considering. | |
100 | |
101 Having optional `new` means that changing a static method to be a constructor is not necessarily a breaking change. Since it's only optional, not disallowed, ch anging in the other direction is a breaking change. | |
102 | |
103 ### Prior discussion | |
104 | |
105 See: http://dartbug.com/5680, http://dartbug.com/18241, http://dartbug.com/20750 . | |
106 | |
107 ### Informal specification | |
108 | |
109 * An expression on one of the forms: | |
110 * `Foo(args)` | |
111 * `Foo<types>(args)` | |
112 * `Foo.bar(args)` | |
113 * `Foo<types>.bar(args)` | |
114 * `prefix.Foo(args)` | |
115 * `prefix.Foo<types>(args)` | |
116 * `prefix.Foo.bar(args)` | |
117 * `prefix.Foo<types>.bar(args)` | |
118 | |
119 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. | |
120 | |
121 * Instead they are equivalent to the same expression with a `new` in front. Th is makes the `new` optional, but still allowed. | |
122 * The grammar allows `prefix.Foo<typeArguments>.bar(args)` and `Foo<typeArgume nts>.bar(args)` as expressions everywhere, not just inside const contexts. These are not valid syntax in the current grammar. | |
123 * Otherwise this is purely syntactic sugar, and existing implementations can h andle this at the syntactic level by inserting a synthetic `new` in front of non -const expressions that would otherwise try to invoke a constructor. This is sta tically detectable. | |
124 | |
125 ## Constructor tear-offs | |
126 | |
127 With constructors being callable like normal static functions, it makes sense to also allow them to be *torn off* in the same way. If `Foo.bar` is a constructor of class `Foo`, then the *expression* `Foo.bar` will be a tear-off of the const ructor (it evaluates to a function with the same signature as the constructor, a nd calling the function will invoke the constructor with the same arguments and an implicit `new`, and return the result). | |
128 | |
129 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. | |
130 | |
131 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. | |
132 | |
133 We could potentially change that, re-purpose the plain `Foo` to refer to the con structor and introduce a new syntax for the `Type` object for the class, say the Java-esque `Foo.class`. It would be a major breaking change, though, even if it could be mechanized. We should consider whether it's feasible to make this chan ge. | |
floitsch
2017/06/19 13:33:48
I would move this into a separate section. "Altern
Lasse Reichstein Nielsen
2017/06/20 11:18:43
Done.
| |
134 | |
135 Otherwise we will introduce the notation `Foo.new`. This is currently a syntax e rror, so it doesn't conflict with any existing code. | |
136 | |
137 For named constructors, an expression like `Foo<int>.bar` (not followed by argum ents like the cases above) is not currently allowed by the syntax, so there is n o conflict. | |
138 | |
139 This tear-off syntax is something we want in any case, independently of the opti onal new/const changes above. However, the syntax completely subsumes the option al `ne` feature; with tear-off syntax, `Foo.bar(42)` is just the tear-off `Foo.b ar` expression called as a function. You'd have to write `Foo.new(42)` instead o f just `Foo(42)` (which is an argument for re-purposing the `Foo` expression to refer to the constructor instead of the type). | |
floitsch
2017/06/19 13:33:48
optional `new` feature. (missing "w")
Lasse Reichstein Nielsen
2017/06/20 11:18:43
Done.
| |
140 That is, if we have constructor tear-offs, the only feature of optional `new` th at isn't covered is calling the unnamed constructor. | |
141 | |
142 | |
143 ### Informal specification | |
144 | |
145 * An expression *x* on one of the forms: | |
146 * `Foo.new` | |
147 * `Foo<types>.new` | |
148 * `Foo.bar` | |
149 * `Foo<types>.bar` | |
150 * `prefix.Foo.new` | |
151 * `prefix.Foo<types>.new` | |
152 * `prefix.Foo.bar` | |
153 * `prefix.Foo<types>.bar` | |
154 | |
155 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 error. | |
156 | |
157 Not included are expressions like `Foo..new(x)` or `Foo..bar(x)`. This is ac tually an argument against adding static cascades (`C..foo()..bar()` isn't curre ntly a static call, it's a cascade on the `Type` object). | |
158 | |
159 * Instead of being an error, the expression evaluates to a function value | |
160 * with the same signature as the constructor (same parameters, default val ues, and having `Foo` or `Foo<types>` as return type), | |
161 * which, when called with `args`, returns the same result as `new x'(args) ` where `x'` is `x` without any `.new`. | |
162 * if `Foo` is not generic, the expression is a canonicalized compile-time constant (like a static method). | |
163 * 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. | |
164 * This feature be *implemented* by adding a static method for each non-generic class constructor: | |
165 | |
166 ```dart | |
167 class C { | |
168 C(x1, …, xn) : … { body } | |
169 static C C_asFunction(x1, … , xn) => new C(x1, … , xn); | |
170 } | |
171 ``` | |
172 | |
173 The tear-off of `C.new` is just `C_asFunction`. | |
174 | |
175 * … and adding a new helper class for each generic class with constructors: | |
176 | |
177 ```dart | |
178 class D<T> { | |
179 D(x1, …, xn) : … { body } | |
180 } | |
181 class D_constructors<T> { | |
182 const D_constructors(); | |
183 D_asFunction(x1, …, xn) => new D<T>(x1, …, xn); | |
184 } | |
185 ``` | |
186 | |
187 Then the tear-off of `D<T>.new` is `const D_constructors<T>().D_asFunction`. If the type `T` is a non-const type parameter, the equality is harder to preser ve, and the implementation might need to cache and canonicalize the `D_construct ors` instances that it does the method tear-offs from, or some other clever hack . | |
188 | |
189 * 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). | |
190 * 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 . | |
191 | |
192 | |
193 ## Optional new/const in *potentially* const expressions | |
194 | |
195 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. | |
196 | |
197 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. | |
198 | |
199 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. | |
200 | |
201 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. | |
202 | |
203 The sub-expressions must still all be potentially const, but that's not a big pr oblem. | |
204 | |
205 It does introduce another problem that is harder to handle - avoiding infinite r ecursion at compile-time. | |
206 | |
207 If a constructor can call another constructor as a potentially constant expressi on, then it's possible to recurse deeply - or infinitely. | |
208 | |
209 Example: | |
210 | |
211 | |
212 ```dart | |
213 class C { | |
214 final int value; | |
215 final C left; | |
216 final C right; | |
217 const C(int start, int depth) | |
218 : left = (depth == 0 ? null : C(start, depth - 1)), | |
219 value = start + (1 << depth), | |
220 right = (depth == 0 ? null : C(start + (1 << depth), depth - 1)); | |
221 } | |
222 ``` | |
223 | |
224 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. | |
225 | |
226 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. | |
227 | |
228 Proposed restriction: Don't allow a constant constructor invocation to invoke th e same constructor again *directly*, where "directly" means: | |
229 | |
230 * as a sub-expression of an expression in the initializer list, or | |
231 * *directly* in the initializer list of another const constructor that is invo ked by a sub-expression in the initializer list. | |
232 | |
233 This transitively prevents the unfolding of the constructor calls to recurse wit hout any limiting constraint. | |
234 | |
235 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: | |
236 | |
237 | |
238 ```dart | |
239 const c0 = const C(0); | |
240 const c43 = const C(43); | |
241 class C { | |
242 final v; | |
243 const C(x) : v = ((x % 2 == 0) ? x : c0); // Silly but valid. | |
244 } | |
245 ``` | |
246 | |
247 | |
248 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. | |
249 | |
250 As usual, a const *variable* cannot refer to itself when its value is evaluated. | |
251 | |
252 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. | |
253 | |
254 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. | |
255 | |
256 This feature is more invasive and complicated than the previous three. If this f eature is omitted, the previous three features still makes sense and should be i mplemented anyway. | |
257 | |
258 ### Prior discussion | |
259 | |
260 See: [issue 18241](http://dartbug.com/18241) | |
261 | |
262 ### Informal specification | |
263 | |
264 In short: | |
265 | |
266 * A const constructor introduces a "potentially const context" for its initial izer list. | |
267 * 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., | |
268 * This means that `const` can be omitted in front of `List` literals, `Map` li terals and constructor invocations. | |
269 * All subexpressions of such expressions must still be *potentially const expr essions*, otherwise it's still an error. | |
270 * 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). | |
271 * 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. | |
272 * This is not just syntactic sugar: | |
273 * 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). | |
274 * 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. | |
275 * 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. | |
276 | |
277 | |
278 | |
279 ## Migration | |
280 | |
281 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. | |
282 | |
283 We will want to migrate library, documentation and example code so they can serv e as good examples. It's not as important as features that affect the actual API . The most visible change will likely be that some constructors can now be torn off as a const expression and used as a parameter default value. | |
284 | |
285 All other uses will occur inside method bodies or initializer expressions. | |
286 | |
287 Removing `new` is easy, and can be done by a simple RegExp replace. | |
288 | |
289 Removing nested `const` probably needs manual attention ("nested" isn't a regula r property). | |
290 | |
291 Using constructor tear-offs will likely be the most visible change, with cases l ike: | |
292 | |
293 | |
294 ```dart | |
295 map.putIfAbsent(42, List<int>.new); // Rather than map.putIfAbsent(42, () => <i nt>[]) | |
floitsch
2017/06/19 13:33:48
map.putIfAbsent(42, HashSet<int>.new); // Rather
Lasse Reichstein Nielsen
2017/06/20 11:18:43
Done.
Also removed the `new` from the "rather than
| |
296 bars.map(Foo.fromBar)... // rather than bars.map((x) => new Foo.fromBar(x)) | |
297 ``` | |
298 | |
299 Once the features are implemented, this can be either done once and for all, or incrementally since each change is independent, but we should plan for it. | |
300 | |
301 ## Related possible features | |
302 | |
303 ### Type variables in static methods | |
304 | |
305 When you invoke a static method, you use the class name as a name-space, e.g., ` Foo.bar()`. | |
306 | |
307 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. | |
308 | |
309 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. | |
310 | |
311 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. | |
312 | |
313 It makes constructors and static methods more symmetric. | |
314 | |
315 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 | |
316 | |
317 | |
318 ```dart | |
319 class C<T extends C<T>> { | |
320 int compare(T other); | |
321 static int compareAny(dynamic o1, dynamic o2) => o1.compare(o2); | |
322 } | |
323 ``` | |
324 | |
325 | |
326 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. | |
327 | |
328 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. | |
329 | |
330 ### Instantiated Type objects | |
331 | |
332 The changes in this document allows `Foo<T>` to occur: | |
333 | |
334 * Followed by arguments, `Foo<T>(args)` | |
335 * Followed by an identifier, `Foo<T>.bar` (and optionally arguments). | |
336 * Followed by `new`, `Foo<T>.new`. | |
337 | |
338 but doesn't allow `Foo<T>` by itself, not even for the non-named constructor. | |
339 | |
340 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). | |
341 | |
342 The syntax will also be useful for instantiated generic method tear-off like `va r intContinuation = future.then<int>;` | |
343 | |
344 ### Generic constructors | |
345 | |
346 We expect to allow generic constructors. | |
347 Currently constructors are not generic the same way other methods are. Instead t hey have access to the class' type parameters, but they can't have separate type parameters. | |
348 | |
349 We plan to allow this for name constructors, so we can write: | |
350 ```dart | |
351 class Map<K, V> { | |
352 … | |
353 factory Map.fromIterable<S>( | |
354 Iterable<S> values, {K key(S value), K value(S value)}) { | |
355 … | |
356 } | |
357 … | |
358 } | |
359 ``` | |
360 Having generic constructors shouldn't add more syntax with optional `new` becaus e it uses the same syntax as generic method invocation. If anything, it makes th ings more consistent. | |
361 | |
362 ### Inferred Constant Expression | |
363 | |
364 An expression like `Duration(seconds: 2)` can be prefixed by either `const` or ` new`. The optional `new` feature would make this create a new object for each ev aluation. | |
365 However, since all arguments are constant and the constructor is `const`, it cou ld implicitly become a `const` expression instead. | |
366 | |
367 This has some consequences – if you actually need a new object each time (say a `new Object()` to use as a marker or sentinel), you would now *have to* write `n ew` to get that behavior. This suggests that if we introduce this feature at all , we should do so at the same time as optional `new`, it would be a breaking cha nge to later change `Object()` from `new` to `const`. | |
368 | |
369 This feature also interacts with optional const. An expression like `Foo(Bar())` , where both `Foo` and `Bar` are `const` constructors, can be either `const` or `new` instantiated. It would probably default to `new`, but writing `const` befo re either `Foo` or `Bar` would make the other be inferred as constant as well. I t's not clear that this is predictable for users (you can omit either, but not b oth `const` prefix without changing the meaning). | |
370 | |
371 ### Revisions | |
372 | |
373 0.5 (2017-02-24) Initial version. | |
374 | |
375 0.6 (2017-06-08) Added "Migration" section, minor tweaks. | |
376 | |
377 0.7 (2017-06-19) Reordered features, added more related features. | |
OLD | NEW |