diff --git a/working/3616 - enum value shorthand/proposal-simple-lrhn.md b/working/3616 - enum value shorthand/proposal-simple-lrhn.md index 46d2a480d..39483e3fe 100644 --- a/working/3616 - enum value shorthand/proposal-simple-lrhn.md +++ b/working/3616 - enum value shorthand/proposal-simple-lrhn.md @@ -1,19 +1,18 @@ # Dart static access shorthand -Author: lrn@google.com
Version: 0.3 -# Dart static access shorthand +Author: lrn@google.com
Version: 1.0 -Pitch: You can write `.foo` instead of `ContextType.foo` when it makes sense. +You can write `.foo` instead of `ContextType.foo` when it makes sense. The rules are fairly simple and easy to explain. ### Elevator pitch -An expression starting with `.` is an implicit static namespaces/class access on the context type. +An expression starting with `.` is an implicit static namespaces access on the *apparent context type*. -The type that the context expects is known, and the expression avoids repeating the type, and gets a value from that type. +The type that the context expects is known, and the expression avoids repeating the type, and starts by doing a static access on that type. -This makes immediate sense for accessing enum and enum-like constants or invoking constructors, which will have the desired type. There is no requirement that the expression ends at that member access, it can be followed by non-assignment selectors. +This makes immediate sense for accessing enum and enum-like constants or invoking constructors, which will have the desired type. There is no requirement that the expression ends at that member access or invocation, it can be followed by non-assignment selectors, and the result just has to have the correct type in the end. The context type used is the one for the entire selector chain. -There must be a context type that allows static member access. +There must be a context type that allows static member access, similar to when we allow static access through a type alias. We also special-case the `==` and `!=` operators, but nothing else. @@ -24,39 +23,61 @@ We also special-case the `==` and `!=` operators, but nothing else. We introduce grammar productions of the form: ```ebnf - ::= ... + ::= ... -- all current productions | - - ::= - `const` '.' ( | 'new') - | '.' ( | 'new') - - ::= ... ;; all the current cases + + ::= ... -- all current productions | + + ::= + '.' ( | 'new') -- shorthand qualified name + | 'const' '.' ( | 'new') -- shorthand object creation ``` -and we add `.` to the tokens that an expression statement cannot start with. _(Just to be safe. If we ever allow metadata on statements, we don’t want `@foo . bar - 4 ;` to be ambiguous. If we ever allow metadata on expressions, we have bigger issues.)_ +We also add `.` to the tokens that an expression statement cannot start with. That means you can write things like the following (with the intended meaning as comments, specification to achieve that below): ```dart -BigInt b0 = .zero; // Getter. Context type BigInt => BigInt.zero -BigInt b1 = b0 + .one; // Getter. Context from BigInt.operator+(BigInt other) => BigInt.one -String s = .fromCharCode(42); // Constructor. Context type String => String.fromCharCode(42) -List l = .filled(10, 42); // Constructor. Context type List => List.filled(10, 42) -int value = .parse(input); // Static function. Context type int => int.parse(input) -Future> futures = .wait([.value(1), .value(2)]); // => -Future> futures = .wait([.value(1), .value(2)]); // => -// Both: Future.wait(>[Future.value(1), Future.value(2)]); +Endian littleEndian = .little; // -> Endian.little (enum value) +Endian hostEndian = .host; // -> Endian.host (getter) +// -> Endian.little, Endian.big, Endian.host +Endian endian = firstWord == 0xFEFF ? .little : firstWord = 0xFFFE ? .big : .host; + +BigInt b0 = .zero; // -> BigInt.zero (getter) +BigInt b1 = b0 + .one; // -> BigInt.one (getter) + +String s = .fromCharCode(42); // -> String.fromCharCode(42) (constructor) + +List l = .filled(10, .big); // -> List.filled(10, Endian.big) + +int value = .parse(input); // -> int.parse(input) (static function) + +Zone zone = .current.errorZone; /// -> Zone.current.errorZone +int posNum = .parse(userInput).abs(); // -> int.parse(userInput).abs() + +// -> Future.wait([Future.value(1), Future.value(2)]) +// (static function and constructors) +Future> futures = .wait([.value(1), .value(2)]); +// -> Future.wait([Future.value(1), Future.value(2)]) +// (static function and constructors) +Future futures = .wait([.value(1), .value(2)]); + +// -> Future>.wait([lazyString(), lazyString()]).then((list) => list.join()) +Future = .wait([lazyString(), lazyString()]).then((list) => list.join()); ``` -This is a simple grammatical change. +This is a simple grammatical change. It allows new constructs in any place where we currently allow primary expressions, which can be followed by selector chains through the `` production ` *`. + +A `` cannot immediately follow any other complete expression. We trust that because a primary expression already contains the production `'(' ')'` which would cause an ambiguity for `e1(e2)` since `(e2)` can also be parsed as a ``. The existing places where a `.` token occurs in the grammar are all in positions where they follow another expression (or qualified identifier), which a primary expression cannot follow. -A primary expression cannot follow any other complete expression, something which would parse as an expression that `e.id` could be a member access on. We know that because a primary expression already contains the production `'(' ')'` which would cause an ambiguity for `e1(e2)` if `(e2)` could also be parsed as a ``. +The `.` token is already a continuation token in the disambiguation rules introduced with the constructor-tear-off feature, which also introduced a single type arguments clause as a selector. That means that `A.id` will always parse `.id` as a selector in that context, and not allow a primary to follow. No new rules are needed. -A primary expression *can* follow a `?` in a conditional expression: `{e1?.id:e2}`. This could be ambiguous, but we handle it at tokenization time by making `?.` a single token, so there is no ambiguity here, just potentially surprising parsing if you omit a space between `?` and `.id`. It’s consistent, and a solved problem. +Therefore the new productions introduces no new grammatical ambiguities. -It avoids being ambiguous with ` ::= *` by only adding the `` if there is a `const` in front. (As long as we don’t introduce `'const' ` as a general operator, the `const .` sequence is unique.) +We prevent expression statements from starting with `.` mainly out of caution. _(It’s very unlikely that an expression statement starting with static member shorthand can compile at all. If we ever allow metadata on statements, we don’t want `@foo . bar(4) ;` to be ambiguous. If we ever allow metadata on expressions, we have bigger issues.)_ + +A primary expression *can* follow a `?` in a conditional expression, `{e1 ? . id : e2}`. This is not ambiguous with `e1?.id` since we parse `?.` as a single token, and will keep doing so. It does mean that `{e1?.id:e2}` and `{e1? .id:e2}` will now both be valid and have different meanings, where the existing grammar didn’t allow the `?` token to be followed by `.` anywhere. ### Semantics @@ -64,46 +85,43 @@ Dart semantics, static and dynamic, does not follow the grammar precisely. For e Because of that, the specification of the static and runtime semantics of the new constructs need to address all the forms .*id*, .*id*\<*typeArgs*\>, .*id*(*args*), .*id*\<*typeArgs*\>(*args*), `.new` or .new(*args*). -_(It also addresses `.new` and `.new(args)`, but those will always be compile-time errors because `.new` denotes a constructor which is not generic. We do not want this to be treated as `(.new)(args)` which does a tear-off of the constructor, which is generic if the class is. That’s bad for readability, and would put `(.new)` in a position with no context type.)_ +_(It also addresses `.new` and `.new(args)`, but those will always be compile-time errors because `.new` denotes a constructor which is not generic. We do not want this to be treated as `(.new)(args)` which creates and calls a generic tear-off of the constructor.)_ -The *general rule* is that any of the expression forms above, starting with .id, are treated exactly *as if* they were prefixed by a fresh variable, *X* which denotes an accessible type alias for the greatest closure of the context type scheme of the expression. We just want to specify that precisely, and handle, and reject, the case of `.id(args)` where `.id` is a getter. +The *general rule* is that any of the expression forms above, starting with .id, are treated exactly *as if* they were prefixed by a fresh identifier *X* which denotes an accessible type alias for the greatest closure of the context type scheme of the following primary and selector chain. #### Type inference -In every place where the current specification specifies type inference for one of the forms *T*.*id*, *T*.*id*\<*typeArgs*\>, *T*.*id*(*args*), *T*.*id*\<*typeArgs*\>(*args*), *T*.new or *T*.new(*args*), where *T* is a type clause or and identifier denoting a type declaration or a type alias declaration, we introduce a parallel “or .id…” clause, and then continue either with the type denoted by *T* as normal, or, for the .*id* clause, with the greatest closure of the context type scheme, and the *`id`* is looked up in that just as one would in the type denoted by *`T`* for *`T.id`*. - -However, the current semantics of `T.id`, `T.id(args)` and `T.id(args)` depends on whether `T.id` is a getter, a method or a constructor. If we only want one layer of invocation, then we should not allow `.getter(args)` to have the same context type for `.getter` as it has for `(args`). Today it doesn’t matter because a getter cannot use its context type for anything. Here we actually want to treat `.getter(args)` as `(.getter)(args)` for type inference, because that’s how it’s analyzed and executed if written as `T.getter(args)`. We just don’t know what `.id` is until we have resolved it to a declaration, which we would be doing *too early* if we did it at `.id(args)`. “Luckily” recognizing that `id` denotes a getter at that point doesn’t mean we have to do a lookup again on `.id` alone, because we know it’ll have an empty context and be a compile-time error. +First, when inferring types for a `` of the form ` *` with context type scheme *C*, then, if the `` has not yet been assigned a *shorthand context*, assign *C* as its shorthand context. Then continue as normal. _This assigns the context type scheme of the entire, maximal selector chain to the static member shorthand, and does not change that when recursing on shorter prefixes._ -So inference works as: +_The effect will be that `.id…` will behave exactly like `T.id…` where `T` denotes the declaration of the context type. -* For any of the expression forms: .new or .new(args), it’s a compile-time error. (The grammar allows writing it, but it must be a constructor invocation, and constructors cannot take type arguments as part of the parameter part.) +**Definition:** If a shorthand context type schema has the form `C` or `C<...>`, and `C` is a type introduced by the type declaration *D*, then the shorthand context *denotes the type declaration* *D*. If a shorthand context `S` denotes a type declaration *D*, then so does a shorthand context `S?`. Otherwise a shorthand context it does not denote any declaration. -* For any of the expression forms: .id, .new, .id(args), .new(args), .id, .id(args) with C being the greatest closure of the context type scheme. +_This effectively derives a *declaration* from the context type scheme of the surrounding ``. It allows a nullable context type to denote the same as its non-`Null` type, so that you can use a static member shorthand as an argument for optional parameters, or in other contexts where we change a type to nullable just to allow omitting things ._ -* If C does not denote a type that can have a static namespace (the type of a class, mixin, enum or extension type declaration, possibly instantiated), it’s a compile-time error. +**Constant shorthand**: When inferring types for a `const .id(arguments)` or `const .new(arguments)` with context type schema *C*, let *D* be the declaration denoted by the shorthand context assigned to the ``. It’s a compile-time error if the shorthand context does not denote a class, mixin, enum or extension type declaration. Then proceed with type inference as if `.id`/`.new` was preceded by an identifier denoting the declaration *D*. It’s a compile-time error if the shorthand context does not denote a class, mixin, enum or extension type declaration. -* Otherwise let D be the member with base name id in the static namespace associated with C. - * It’s a compile-time error if there is no such declaration. +**Non-constant shorthand**: When inferring types for constructs containing the non-`const` production, in every place where the current specification specifies type inference for one of the forms *T*.*id*, *T*.*id*\<*typeArgs*\>, *T*.*id*(*args*), *T*.*id*\<*typeArgs*\>(*args*), *T*.new, *T*.new(*args*), *T*.new\<*typeArgs*\> or *T*.new\<*typeArgs*\>, where *T* is a type literal, we introduce a parallel “or .id…” clause for a similarly shaped ``, proceeding as if `.id`/`.new` was preceded by an identifier denoting the declaration that is denoted by the shorthand context assigned to the leading ``. It’s a compile-time error if the shorthand context does not denote a class, mixin, enum or extension type declaration. -* If D is a getter and the expression is of the form .id(args), id or .id(args), it’s a compile-time error. (The invocation will be treated as (.id)(args) etc., which puts .id in a position with no context type, which is an error.) +Expression forms `.new` or `.new(args)` will always be compile-time errors. (The grammar allows them, because it allows any selector to follow a static member shorthand, but that static member shorthand must denote a constructor invocation, and constructors cannot, currently, be generic.) -* Otherwise treat the expression as if it was a static getter, function or constructor invocation of the member D on the type C. For constructors of a generic type, that includes any type arguments in C. For methods and getters it means a static invocation of the getter or of the function with the provided arguments. (The same ways that X.id… would work where X is a type alias for C. We should refactor the spec so we can refer to “static inference of a resolved static function/constructor with/without arguments” and then reference that from both, .id, T.id and id-in-scope after we have resolved the target to a static declaration or constructor) +**Notice**: The invocation of a constructor is *not* using an instantiated type, it’s behaving as if the constructor was preceded by a *raw type*, which type inference should then infer type arguments for. Doing `List l = .filled(10, 10);` works like doing `List l = List.filled(10, 10);`, and it is the following downwards inference with context type `List` that makes it into `List.filled(10, 10);`. This distinction matters for something like: -* The static type of the expression is the return type of the getter, the inferred return type of the function after doing type inference on the arguments and inferring type arguments and return type for the function invocation, and C for a constructor invocation. +```dart +List l = .generate(10, (int i) => i + 1).map((x) => x.toRadixString(16)).toList(); +``` -Whichever static member or constructor the *`.id`* denotes, it is remembered for the runtime semantics. +which is equivalent to inserting `List` in front of `.filled`, which will then be inferred as `List`. In most normal use-cases it doesn’t matter, because the context type will fill in the missing type variables, but if the construction is followed by more selectors, it loses that context type. _It also means that the meaning of `.id`/`.new` is *always* the same, it doesn’t matter whether it’s a constructor or a static member, it’s always preceded by the name of the declaration denoted by the context. -That means that the following are *not* allowed: +The following uses are *not* allowed because they have no shorthand context that denotes an allowed type declaration: ```dart // NOT ALLOWED, ALL `.id`S ARE ERRORS! int v1 = .parse("42") + 1; // Context `_` -int v2 = .parse("42").abs(); // Context `_` -Zone zone = .current.errorZone; // Context `_` -extension on Object? { - int call() => "${this}".length; // Call *all* the objects! -} -double x = .nan(); // Is getter, treated as Context `_` when recognized. +int v2 = (.parse("42")).abs(); // Context `_` +dynamic v3 = .parse("42"); // Context `_` +int? v1 = .parse("42"); // Context `int?`, does not have the form `C` or `C`. +FutureOr = .parse("42"); // Context `FutureOr` has form `C`, but wrong kind of type. ``` #### Special case for `==` @@ -112,26 +130,18 @@ For `==`, we special case the context type that is applied to a `.id`. If an expression has the form `e1 == e2` or `e1 != e2` , then -* If `e1` starts with an implicit static access then: +* If `e1` has the form ` ` then: * Let *S2* be the static type of `e2` with context type scheme `_`. - * Let *S1* be the static type of `e1` with context type *S2*. + * Let *S1* be the static type of `e1` with context type *S2* (which will assign *S2* as the shorthand context). * Let *R* Function(*T*) be the function signature of `operator==` of *S1*. * Otherwise: * Let *S1* be the static type of `e1` with context type scheme `_`. * Let *R* Function(*T*) be the function signature of `operator==` of *S1*. - * If `e2` *starts with an implicit static access*, and *T* is a supertype of `Object`, then let *S2* be the static type of `e2` with context type *S1*. + * If *S2* has the form ` *` and *T* is a supertype of `Object`, then let *S2* be the static type of `e2` with context type *S1*. * Otherwise let *S2* be the static type of `e2` with context type *T*?. * It’s a compile-time error if *S2* is not assignable to *T*?. _(Notice, we do not require the expression to match the context type of S1, that is a *recommended* type only, a typing hint.)_ * The static type of the expression is *R*. -An expression *starts with an implicit static access* if and only if one of the following: - -* The expression is an `` optionally followed by a single `` and an optional ``. -* The expression is `(e)` and `e` starts with an implicit static access. -* The expression is `e..` or `e?..` and `e` starts with an implicit static access. -* The expression is `e1 ? e2 : e3` and at least one of `e2` or `e3` starts with an implicit static access. -* *(Any more? It’s an approximation of “in tail-like position”, which should mean that the the implicit static access should get the context type of the entire expression.)* - Examples of allowed comparisons: ```dart @@ -144,27 +154,27 @@ Not allowed: ```dart // NOT ALLOWED, ALL `.id`S ARE ERRORS -if (.host == .big) notOk!; -if ((Endian.host as Object) == .little) notOk!; // Context type `Object`. +if (.host == .big) notOk!; // `.big` is inferred with context type `_`. +if ((Endian.host as Object) == .little) notOk!; // Context type `Object`, no `Object.little`. ``` -It’s possible that this rule is too complicated, and we should just drop the first part, and only use the type of the first operand as context type for the second operand if what we have is no better than `Object`. Then tell people that they can only have `.foo` on in the second operand of `==`, which is the same as for example `+`, like `BigInt.zero + .one`. +_It’s possible that this rule is too complicated, and we should just drop the first part, and only use the type of the first operand as context type for the second operand if what we have is no better than `Object`. Then tell people that they can only have `.foo` as the second operand of `==`, which is the same as for example `+`, like `BigInt.zero + .one`._ #### Runtime semantics -Similar to type inference, in every place where we specify an explicit static member access or invocation, we introduce a clause including the .*id*… variant too, the “implicit static access”, and refer to type inference for “the declaration denoted by *id* as determined during type inference”, then invoke it the same way an explicit static access would. +In every place in type inference where we used the assigned shorthand context to decide which static namespace to look in, we remember the result of that lookup, and at runtime we invoke that static member. _Like we may infer type arguments to constructors, and use those as runtime type arguments to the class, we infer the entire target of the member access and use that at runtime._ -If we get here, it’s just invoking a known static member as normal. +In every case where we inserted a type inference clause, we resolved the reference to a static member in order to use its type for static type inference. The runtime semantics then say that it invokes the member found before, and it works for the `.id…` variant too. #### Patterns -A *constant pattern* is treated the same as normal, with the matched value type used as typing context, and then the expression must be a constant expression. Since a constant pattern cannot occur in a declaration pattern, there is no need to assign an initial type scheme to the pattern in the first phase of the three-step inference. _If there were, the type scheme would be `_`._ +A *constant pattern* is treated the same as any other constant expression, with the matched value type used as the context type schema that is assigned as shorthand context. Since a constant pattern cannot occur in a declaration pattern, there is no need to assign an initial type scheme to the pattern in the first phase of the three-step inference. _If there were, the type scheme would be `_`._ Example: ```dart switch (Endian.host) { - case .big: // Context type is matched value type, which is `Endian` => `Endian.big`. + case .big: // Matched value type = Context type is `Endian` -> `Endian.big`. case .little: // => `Endian.little` } ``` @@ -184,113 +194,112 @@ Endian endian = .big; // => Endian.big. ## New complications and concerns +### Delayed resolution + The `.id` access is a static member access which cannot be resolved before type inference. Prior to this feature, static member accesses could always be resolved using only the lexical scopes and declaration namespaces, which does not require type inference. -Similarly, it’s not known whether `.id` is a valid potentially constant or constant expression until it’s resolved what it refers to. This may delay some errors until after type inference. +Similarly, it’s not known whether `.id` is a valid potentially constant or constant expression until it’s resolved what it refers to. This may delay some errors until after type inference that could previously be given earlier. -It’s not clear that this causes any problems, but it may need implementations to adapt, if they assumed that all static member accesses could be known (and the rest tree-shaken) before type inference. +It’s not clear that this causes any problems, but it may need implementations to adapt, if they assumed that all static member accesses could be known (and the rest tree-shaken eagerly) before type inference. With this feature, static member access, like instance member access, may need types to decide which static declarations are possible targets. -## Possible variations +### Declaration kinds -### Grammar +The restriction “It’s a compile-time error if the shorthand context does not denote a class, mixin, enum or extension type declaration” makes it a visible property of a declaration whether it is one of these. -Instead of introducing a new primary, we can make it a ``: +Prior to this feature, there are types where it’s *unspecified* whether they are introduced by class declarations or not. These are all types that you cannot extend, implement or mix in, so there is nothing you can *use* them for that would be enabled or prevented by being or not being, for example, a class. -```ebnf - ::= - | * - | * - - ::= | 'new') - | '.' ( | 'new') - - ::= ... ;; all the current cases - | -``` +This may require us to *specify* which platform types are considered introduced by which kind of declaration, because it now matters. Or we can do nothing, and pretend there is no issue. Structural types (nullable, `FutureOr`, function types), `dynamic`, `void` and `Never` do not have any static members, so it doesn’t matter whether you allow a static member shorthand access on them, it’ll just fail to find anything. -where we recognize a ` ` and use the context type of the entire selector chain as the lookup type for the `.id`, and it can then continue doing things after than, like: +Basically, we need a term for “a type (schema) which denotes a static namespace”. That is what the shorthand context type schema must do. -```dart -Range range = .new(0, 100).translate(userDelta); -BigInt p64 = .two.pow(64); -``` +## Possible variations and future features -Here the `.new(0, 100)` itself has no context type, but the entire selector chain does, and that is the type used for resolving `.new`. +### Static extensions -This should allow more expressions to use the context. It may make it easier to get a *wrong* result, but you can do that in the first step if `.foo()` returns something of a wrong type. +If we add static extensions to the language, they should work with static member shorthands. After we have decided which namespace to look in, based on the shorthand context, everything should work exactly as if that namespace had been written explicitly, including static extension member access. -This also *avoids* the complication of having to recognize `.getter(args)` and disallow it. It works, if it has the correct type. It won’t be possible to have `?.id` or `?[e]` selectors in the chain because that makes the static type nullable, and the context type is not nullable if it allows static member lookup _(but see below on nullability)_. +This should “just work”, and having static extensions would significantly increase the value of this feature, by allowing users to introduce their own shorthands for any interface type. -This syntax still does *not* allow other operators than the selector ones (index operator, call operator if we consider it one), so the following are invalid: +#### Nullable types and `Null` -```dart -BigInt m2 = -.two; // INVALID (No context type for operand of unary `-`) -BigInt m2 = .two + .one; // INVALID (No context type for first operand of `+`) -``` +##### Why allow nullable types to begin with -_(We could go further and look at the syntactically first *primary expression* in the entire expression, and apply the full context type to that, if it gets no context type otherwise. Something like `!e` gives `e` a real context type of `bool`, but `.foo + 2` and `-.foo` do not. It’s much more complicated, though.)_ +It is a conspicuous special-casing, but it’s of a type that we otherwise special-case all the time. -An expression of the form `.foo.bar.baz` might not be considered an *assignable* expression, so you can’t do `SomeType v = .current = SomeType();`. An expression of the form `.something` should *produce* the value of the context type, that’s why it’s based on the context type, and an assignment produces the value of its right-hand side, not of the assignable expression. +It allows `int? v = .tryParse(42);` will work. That’s a *pretty good reason*.
It also allows `int x = .tryParse(input) ?? 0;` to work, which it wouldn’t otherwise because the context type of `.tryParse(input)` is `int?`. -On the other hand, an assignable expression being assigned to gets no context type, so that would automatically not work, and we may not need to make an exception. +We generally treat the nullable and non-nullable type as closely related (if one is a type of interest, so is the other), and we treat `T?` as meaning “optional `T`”. It makes good sense to supply a `T` where an optional `T` is expected. -And for `.current ??= []`, which *sometimes* does and sometimes doesn’t produce the value, it might be convenient. However, the context type of `.current` in `Something something = .current ??= Something(0);` will be `Something?`, a union type which does not denote a type declaration, so it wouldn’t work either. For `Something something = .current += 1;`, the context type of `.current` needs to be defined. It probably has none today because assignable expressions cannot use a context type for anything. +If we didn’t allow it, it would make a difference whether you declare your method as: -All in all, it doesn’t seem like an implicit static member access can be assigned to and have a useful context type at the same time, so effectively they are not assignable. +```dart +void foo([Foo? foo]) { foo ??= const Foo(null); ... } +``` -#### Nullable types +or -Should a nullable context type, `Foo?` look for members in `Foo`. Or in `Foo` *and* `Null`. (Which will make more sense when we get static extensions which can add members on `Null`.) +```dart +void foo([Foo foo = const Foo(null)]) { ... } +``` -It would allow `Foo x = .current ?? Foo(0);` to work, which it doesn’t today when the context type of `.current` is `Foo?`, and a union type doesn’t denote a static namespace otherwise. +which are both completely valid ways to write essentially the same function. The latter can be called as `foo(.someFoo)` and the former cannot, but the former can be called with `null`, which is why you might want it. This way, you can use the latter and allow both `null` and `.someFoo` as arguments. -If we don’t allow it, it then makes a difference whether you declare your method as: +##### Statics on `Null` -```dart -void foo([Foo? foo]) { - foo ??= const Foo(null); - // ... -} -``` +The type `int?` is a union type, but we only allow members on `int`. Should we *also* allow static members on `Null`, checking both to see which one has a member of the given base name, and then resolve to that? (And a compile-time error in case both has one.) -or +*Currently* it makes no difference because `Null` has no static members. If/when we introduce static extensions, that may change. + +We should consider, no later than at that time, whether a nullable type should allow access to members of `Null`. + +It’s *probably safe* to do so, and it means that the **Norm**-equivalent `Never?` and `Null` have the same members (since `Never` doesn’t have any). It’s also unlikely that there will be many methods on `Null`, but it does allow things like: ```dart -void foo([Foo foo = const Foo(null)]) { - // ... +static extension on Null { + static T? maybe(bool test, T value) => test ? value : null; } +... + String? v = .maybe(someTest, "Bananas"); ``` -which are both completely valid ways to write essentially the same function. It makes a difference because the latter can be called as `foo(.someFoo)` and the former cannot, and the former can be called with `null`, which is why you might want it. If we don’t allow shorthands with nullable context types, we effectively encourage people to write in the latter style, and it’s a usability pitfall to use the former with an enum type. I’m not sure we *want* to cause that kind of forced choices on API design. The shorthand shouldn’t punish you for an otherwise reasonable choice. +Putting extensions on `Null` makes them shorthands on *every nullable type*. That might be a little more power than we are intending this feature to have. Or it might be marvelous. + +We *can* choose to only let nullable types provide their non-`Null` statics as shorthands. That’s the *intent*, to provide an optional value, not as a way to act on optionality itself. -So leaning on allowing. +(For now, it doesn’t matter.) #### Asynchrony and other element types The nullable type is a union type. So is `FutureOr`. -If we allow the nullable context to access members on the type (and on `Null`), should we allow static members of `Foo` (and `Future`) to be accessed with that as context type? +Should we allow a context type of `FutureOr` to access static members on `Foo`? -It’s useful. Until [#870](https://dartbug.com/language/870) gets done, the return type of a return expression in an `async` function is `FutureOr` where `F` is the future-value-type of the function. If we don’t allow access, then changing `Foo foo() => .value;` to `Future foo() async => .value;` will not work. That’s definitely going to be a surprise to users, and it’s a usability cliff. And telling them to do `Foo result = .value; return result;` instead of `return .value;` goes against everything we have so far tried to teach. +If we allow the nullable context to access static members on `Null`, should we allow `FutureOr` to access static members `Future`? -Same applies to `Future f = Future.value(.someValue);` where `Future.value` which also takes `FutureOr` as argument. That would be an argument for having a *real* `Future.valueOnly(T value) : …`, and it’s too bad the good name is taken. +It’s useful. Until [#870](https://dartbug.com/language/870) gets done, the context type of a return expression in an `async` function is `FutureOr` where `F` is the future-value-type of the function. If we don’t allow static access to `Foo` members, then changing `Foo foo() => .value;` to `Future foo() async => .value;` will not work. That’s definitely going to be a surprise to users, and it’s a usability cliff. And telling them to do `Foo result = .value; return result;` instead of `return .value;` goes against everything we have so far tried to teach. (Or get \#870 fixed). -If we say that a type is the authority on creating instances of itself, it *might* also be an authority on creating those instances *asynchronously*. With a context type of `Future`, should we check the `Foo` declaration for a `Future`-returning function, or just the `Future` class? If do we check `Foo`, we should probably check be both. +Same applies to `Future f = Future.value(.someValue);` where `Future.value` which also takes `FutureOr` as argument. That would be an argument for having a *real* `Future.valueOnly(T value) : …`, and it’s too bad the good name is taken. _(And so is `Future(…)` for a variant that is almost never used.)_ + +If we say that a type is the authority on creating instances of itself, which is why we want to allow calling constructors, it *might* also be an authority on creating those instances *asynchronously*. With a context type of `Future`, should we check the `Foo` declaration for a `Future`-returning function, or just the `Future` class? If do we check `Foo`, we should probably check be both. If we allow a static member of `Foo` to be accessed on `FutureOr`, and to return a `Future`, but do not allow that with a context type of `Future`, it punishes people for being specific. It would *encourage* using `FutureOr` as type instead of `Future`, to make the API more user friendly. So, if we allow shorthand `Foo` member access on `FutureOr`, we *may* want to allow it on `Future` too. (But not on more specialized subtype of `Future`, like `class MyFuture implements Future …`.) This gets even further away from being simple, and it special cases the `Future` type, which isn’t *that* special as a type. (It’s not a union type. It is very special *semantically*, an asynchronous function is a completely different kind of function than a synchronous one, and `Future` is really a way of saying “`Foo`, but later”. But the type is just another type.) -If we don’t consider `Future` to be special in the language, then allowing shorthand access to `Foo` members on `Future` can also be used, using the same arguments, for allowing it on `List`. - -For enums, that’s even useful: `var fooSet = EnumSet(.values)` which expects a `List` as argument. +If we don’t consider `Future` to be special in the language, and we allow shorthand access to `Foo` members on `Future`, any argument for that can also be used for allowing `Foo` member access on it on `List`. For enums, that’s even useful: `var fooSet = EnumSet(.values)` which expects a `List` as argument. -So probably a “no” to `Future` and therefore `FutureOr`. But it is annoying because of the implicit `FutureOr` context types. (Maybe we can special case `.foo` in returns of `async` functions only.) +It’s probably a “no” (bordering on “heck no!”) to `Future` and therefore probably to `FutureOr` too. But it is annoying because of the implicit `FutureOr` context types. (Maybe we can special case `.foo` in returns of `async` functions only.) ## Versions + +1.0: Switches to alternative version, where context type applies to selector chain. + +* Changes semantics to insert a non-instantiated type as the namespace reference. + * Means `.foo` is always equivalent to `SomeType.foo`, whether it’s a constructor or not. Inference will apply constructor type arguments. If you write `SomeType v = .id…;` it means `SomeType v = SomeType.id…;`, every time. +* Allows `Foo` static member access on a `Foo?` context type. It’s too convenient to ignore. + 0.3: More details on type inference and examples. 0.2: Updated with more examples and more arguments (in both directions) in the union type sections. 0.1: First version, for initial comments.