| ## Feature: Super-bounded Types |
| |
| **Author**: eernst@. |
| |
| **Version**: 0.8 (2018-10-16). |
| |
| **Status**: Background material. |
| The language specification has the normative text on this topic. |
| Note that the rules have changed, which means that |
| **this document cannot be used as a reference**, it can only be |
| used to get an overview of the ideas; please refer to the language |
| specification for all technical details. |
| |
| **This document** is an informal specification of the support in Dart 2 for |
| using certain generic types where the declared bounds are violated. The |
| feature described here, *super-bounded types*, consists in allowing an |
| actual type argument to be a supertype of the declared bound, as long as a |
| consistent replacement of `Object`, `dynamic`, and `void` by `Null` |
| produces a traditional, well-bounded type. For example, if a class `C` |
| takes a type argument `X` which must extend `C<X>`, `C<Object>`, |
| `C<dynamic>`, and `C<void>` are correct super-bounded types. This is useful |
| because there is no other way to specify a type which retains the knowledge |
| that it is a `C` based type, and at the same time it is a supertype of |
| `C<T>` for all the `T` that satisfy the specified bound. In other words, it |
| allows developers to specify that they want to work with a `C<T>`, except |
| that they don't care which `T`, and _every_ such `T` must be allowed. |
| |
| This permission to use a super-bounded type is only granted in some |
| situations. For instance, super-bounded types are allowed as type |
| annotations, but they are not allowed in instance creation expressions like |
| `new C<Object>()` (assuming that `Object` violates the bound of |
| `C`). Similarly, a function declared as |
| ```dart |
| void foo<X extends List<num>>(X x) { |
| ... |
| } |
| ``` |
| cannot be invoked with `foo<List<dynamic>>([])`, nor can the type |
| argument be inferred to `List<dynamic>` in an invocation like |
| `foo([])`. But `C<void> x = new C<int>();` is OK, and so is |
| `x is C<Object>`. |
| |
| |
| ## Motivation |
| |
| Many well-known classes have a characteristic typing structure: |
| ```dart |
| abstract class num implements Comparable<num> {...} |
| class Duration implements Comparable<Duration> {...} |
| class DateTime implements Comparable<DateTime> {...} |
| ... |
| ``` |
| The class `Comparable<T>` has a method `int compareTo(T other)`, |
| which makes it possible to do things like this: |
| ```dart |
| int comparison = a.compareTo(b); |
| ``` |
| This works fine when `a` and `b` both have type `num`, or both have type |
| `Duration`, but it is not so easy to describe the situation where the |
| comparable type can vary. For instance, consider the following: |
| ```dart |
| class ComparablePair<X extends Comparable<X>> { |
| X a, b; |
| } |
| |
| main() { |
| ComparablePair<MysteryType> myPair = ... |
| int comparison = myPair.a.compareTo(myPair.b); |
| } |
| ``` |
| We could replace `MysteryType` by `num` and then work on pairs |
| of `num` only. But can we find a type to replace `MysteryType` such |
| that `myPair` can hold an instance of `ComparablePair<T>`, no matter |
| which `T` it uses? |
| |
| We would need a supertype of all those `T` where |
| `T extends Comparable<T>`; but we cannot use the obvious ones like |
| `Object` or `dynamic`, because they do not satisfy the declared |
| bound for the type argument to `ComparablePair`. There is in fact no such |
| type in the Dart type system! |
| |
| This is an issue that comes up in various forms whenever a type parameter |
| bound uses the corresponding type variable itself (or multiple type |
| parameters mutually depend on each other), that is, whenever we have one |
| or more _F-bounded_ type parameters. Here is an example which is concise |
| and contains the core of the issue: |
| ```dart |
| class C<X extends C<X>> { |
| X next; |
| } |
| ``` |
| For each given type `T` it is possible to determine whether `T` is a |
| subtype of `C<T>`, in which case that `T` would be an admissible actual |
| type argument for `C`. This means that the set of possible values for `X` |
| is a well-defined set. |
| |
| However, there is no type `S` such that the set of possible values for `X` |
| is equal to the set of subtypes of `S`; that is, the set of types we seek |
| to express is not the set of subtypes of anything. Sure, those types are a |
| subset of all subtypes of `Object`, but we need to express that _exact_ set |
| of types, not a superset. |
| |
| Hence, we cannot correctly characterize "all possible values for `X`" as a |
| single type argument. This means that we cannot find a `U` such that `C<U>` |
| is the least upper bound of all possible types on the form `C<T>`. |
| |
| But that's exactly what we _must_ find, if we are to safely express the |
| greatest possible amount of information about the set of objects whose type |
| is on the form `C<T>` for some `T`. In particular, we cannot express the |
| type which "should be" the result of |
| [instantiate-to-bound](https://github.com/dart-lang/sdk/blob/master/docs/language/informal/instantiate-to-bound.md) |
| on the raw type `C`. |
| |
| We can make an attempt to approximate the least supertype of all correct |
| generic instantiations of `C` (that is, a supertype of all types on the |
| form `C<T>`). Assume that `T` is an admissible actual type argument for |
| `C` (that is, we can rely on `T extends C<T>`): |
| ```dart |
| // Because `T extends C<T>`, and due to generic covariance: |
| C<T> <: C<C<T>> |
| |
| // Same facts used on the nested type argument `T`: |
| C<C<T>> <: C<C<C<T>>> ... |
| |
| // Same at the next level ... |
| C<C<C<T>>> <: C<C<C<C<T>>>> |
| ... |
| ``` |
| We can continue ad infinitum, and this means that a good candidate for the |
| "least upper bound of all `C<T>`" would be the infinite type `W` where |
| `W = C<W>`. Basically, `W = C<C<C<C<C<C<...>>>>>>`, nesting to an |
| infinite depth. |
| |
| Note that `T` "disappears" when we extend the nesting ad infinitum, which |
| means that `W` is the result we find for _every_ `T`. Conversely, we cannot |
| hope to find a different type `V` (not equal to `C<R>` for any `R`) such |
| that `V` is both a supertype of all types on the form `C<T>` for some `T` |
| and `V` is a proper subtype of `W`. In other words, if the "least upper |
| bound of all `C<T>`" exists, it must be `W`. |
| |
| However, we do not wish to introduce these infinite types into the Dart |
| type universe. The ability to express types on this form will |
| inevitably introduce the ability to express many new kinds of types, and we |
| do not expect this generalization to improve the expressive power of the |
| language in a manner that compensates sufficiently for the burden of |
| managing the added complexity. |
| |
| Instead, we give developers the responsibility to make the choice |
| explicitly: They can use super-bounded types to express a range of |
| supertypes of these infinite types (as well as other types, if they |
| wish). When they do that with an infinite type, they can make the choice to |
| unfold it exactly as many times as they want. At the same time, they will |
| be forced to maintain a greater level of awareness of the nature of these |
| types than they would, had we chosen to model infinite types, e.g., by |
| unfolding them to some specific, finite level. |
| |
| Here are some examples of finite unfoldings, and the effect they have on |
| types of expressions: |
| ```dart |
| class C<X extends C<X>> { |
| X next; |
| C(this.next); |
| } |
| |
| class D extends C<D> { |
| D(D next): super(next); |
| } |
| |
| main() { |
| D d = new D(new D(null)); |
| C<dynamic> c0 = d; |
| C<C<dynamic>> c1 = d; |
| C<C<C<dynamic>>> c2 = d; |
| |
| c0.next.unknown(42); // Statically OK, `c0.next` is `dynamic`. |
| c1.next.unknown(43); // Compile-time error. |
| c1.next.next.unknown(44); // Statically OK. |
| c2.next.next.unknown(45); // Compile-time error. |
| c2.next.next.next.unknown(46); // Statically OK. |
| |
| // With type `D`, the static analysis is aware of the cyclic |
| // structure of the type, and every level of nesting is handled |
| // safely. But `D` may be less useful because there may be a |
| // similar type `D2`, and this code will only work with `D`. |
| d.next.next.next.next.next.next.next.unknown(46); // Compile-time error. |
| } |
| ``` |
| We can make a choice of how to deal with the missing type information. When |
| we use `C<dynamic>`, `C<C<dynamic>>` and `C<C<C<dynamic>>>` we will |
| implicitly switch to dynamic member access after a few steps of |
| navigation. |
| |
| If we choose to use `C<Object>`, `C<C<Object>>` and so on then we will have |
| to use explicit downcasts in order to access all non-`Object` members. We |
| will still be able to pass `c0.next` as an argument to a function expecting |
| a `C<S>` (where `S` can be anything), but we could also pass it where a |
| `String` is expected, etc. |
| |
| Finally, if we choose to use `C<void>` and so on then we will not even be |
| able to access the object where the type information ends: we cannot use |
| the value of an expression like `c0.next` without an explicit |
| cast (OK, `void v = c0.next;` is accepted, but it is mostly impossible to |
| use the value of an expression of type `void`). This means that we cannot |
| pass `c0.next` as an argument to a function that accepts a `C<S>` |
| (for any `S`) without an explicit cast. |
| |
| In summary, the choice of `dynamic`, `Object`, and `void` offers a range of |
| approaches to the lack of typing information, but the amount of information |
| remains the same. |
| |
| |
| ## Syntax |
| |
| This feature does not require any modifications to the Dart grammar. |
| |
| |
| ## Static analysis |
| |
| We say that the parameterized type _G<T<sub>1</sub>..T<sub>k</sub>>_ is |
| _regular-bounded_ when _T<sub>j</sub> <: [T<sub>1</sub>/X<sub>1</sub> .. |
| T<sub>k</sub>/X<sub>k</sub>]B<sub>j</sub>_ for all _j_, _1 <= j <= k_, |
| where _X<sub>1</sub>..X<sub>k</sub>_ are the formal type parameters of _G_ |
| in declaration order, and _B<sub>1</sub>..B<sub>k</sub>_ are the |
| corresponding upper bounds. |
| |
| *This means that each actual type argument satisfies the declared upper |
| bound for the corresponding formal type parameter.* |
| |
| We extend covariance for generic class types such that it can be used also |
| in cases where a type argument violates the corresponding bound. |
| |
| *For instance, assuming the classes `C` and `D` as declared in the |
| Motivation section, `C<D>` is a subtype of `C<Object>`. This is new because |
| `C<Object>` used to be a compile-time error, which means that no questions |
| could be asked about its properties. Note that this is a straightforward |
| application of the usual covariance rule: `C<D> <: C<Object>` because |
| `D <: Object`. We need this relaxation of the rules in order to be able to |
| define which violations of the declared bounds are admissible.* |
| |
| Let _G_ denote a generic class, _X<sub>1</sub>..X<sub>k</sub>_ the formal |
| type parameters of _G_ in declaration order, and |
| _B<sub>1</sub>..B<sub>k</sub>_ the types in the corresponding upper bounds, |
| using `Object` when the upper bound is omitted. The parameterized type |
| _G<T<sub>1</sub>..T<sub>k</sub>>_ is then a _super-bounded type_ |
| iff the following two requirements are satisfied: |
| |
| 1. There is a _j_, _1 <= j <= k_, such that _T<sub>j</sub>_ is not a |
| subtype of |
| _[T<sub>1</sub>/X<sub>1</sub>..T<sub>k</sub>/X<sub>k</sub>]B<sub>j</sub>_. |
| |
| 2. Let _S<sub>j</sub>_, _1 <= j <= k_, be the result of replacing every |
| covariant occurrence of `Object`, `dynamic`, and `void` in |
| _T<sub>j</sub>_ by `Null`, and every contravariant occurrence of `Null` |
| by `Object`. It is then required that |
| _S<sub>j</sub> <: |
| [S<sub>1</sub>/X<sub>1</sub>..S<sub>k</sub>/X<sub>k</sub>]B<sub>j</sub>_ |
| for all _j_, _1 <= j <= k_. |
| |
| *In short, at least one type argument violates its bound, and the type is |
| regular-bounded after replacing all occurrences of an extreme type by the |
| opposite extreme type, according to their variance.* |
| |
| *For instance, assuming the declarations of `C` and `D` as in the |
| Motivation section, `C<Object>` is a super-bounded type, because `Object` |
| violates the declared bound and `C<Null>` is regular-bounded.* |
| |
| *Here is an example that involves contravariance:* |
| |
| ```dart |
| class E<X extends void Function(X)> {} |
| ``` |
| |
| *With this declaration, `E<void Function(Null)>` is a super-bounded type |
| because `E<void Function(Object)>` is a regular-bounded type. Note that |
| the contravariance can also be eliminated, yielding a simpler super-bounded |
| type: `E<dynamic>` is a super-bounded type because `E<Null>` is a |
| regular-bounded type.* |
| |
| We say that a parameterized type _T_ is _well-bounded_ if it is |
| regular-bounded or super-bounded. |
| |
| *Note that it is possible for a super-bounded type to be nested in another |
| type which is super-bounded, and it can also be nested in another type |
| which is not super-bounded. For example, assuming `C` as in the Motivation |
| section, `C<C<Object>>` is a super-bounded type which contains a |
| super-bounded type; in contrast, `List<C<Object>>` is a regular type (a |
| generic instantiation of `List`) which contains a super-bounded type |
| (`C<Object>`).* |
| |
| It is a compile-time error if a parameterized type is not well-bounded. |
| |
| *That is, a parameterized type is regular-bounded, or it is super-bounded, |
| or it is an error. This rule replaces and relaxes the rule in the language |
| specification that constrains parameterized types to be regular-bounded.* |
| |
| It is a compile-time error if a type used as the type in an instance |
| creation expression (*that is, the `T` in expressions of the form |
| `new T(...)`, `new T.id(...)`, `const T(...)`, or `const T.id(...)`*) |
| is super-bounded. It is a compile-time error if the type in a redirection |
| of a redirecting factory constructor (*that is, the `T` in a phrase of the |
| form `T` or `T.id` after `=` in the constructor declaration*) is |
| super-bounded. It is a compile-time error if a super-bounded type is |
| specified as a superinterface for a class. (*This implies that a |
| super-bounded type cannot appear in an `extends`, `implements`, or |
| `with` clause, or in a mixin application; e.g., `T` in |
| `class C = T with M;` cannot be super-bounded*). Finally, it is a |
| compile-time error if a bound in a formal type parameter declaration is |
| super-bounded. |
| |
| *This means that we allow super-bounded types as function return types, as |
| type annotations on variables (all of them: library, static, instance, and |
| local variables, and formal parameters of functions), in type tests |
| (`e is T`), in type casts (`e as T`), in `on` clauses, and as type |
| arguments.* |
| |
| Let _F_ denote a parameterized type alias, _X<sub>1</sub>..X<sub>k</sub>_ the |
| formal type parameters of _F_ in declaration order, and |
| _B<sub>1</sub>..B<sub>k</sub>_ the types in the corresponding upper bounds, |
| using `Object` when the upper bound is omitted. The parameterized type |
| _F<T<sub>1</sub>..T<sub>k</sub>>_ is then a _super-bounded type_ |
| iff the following three requirements are satisfied: |
| |
| 1. There is a _j_, _1 <= j <= k_, such that _T<sub>j</sub>_ is not a |
| subtype of |
| _[T<sub>1</sub>/X<sub>1</sub>..T<sub>k</sub>/X<sub>k</sub>]B<sub>j</sub>_. |
| |
| 2. Let _S<sub>j</sub>_, _1 <= j <= k_, be the result of replacing every |
| covariant occurrence of `Object`, `dynamic`, and `void` in |
| _T<sub>j</sub>_ by `Null`, and every contravariant occurrence of `Null` |
| by `dynamic`. It is then required that |
| _S<sub>j</sub> <: |
| [S<sub>1</sub>/X<sub>1</sub>..S<sub>k</sub>/X<sub>k</sub>]B<sub>j</sub>_ |
| for all _j_, _1 <= j <= k_. |
| |
| 3. Let _T_ be the right hand side of the declaration of _F_, then |
| _[T<sub>1</sub>/X<sub>1</sub>..T<sub>k</sub>/X<sub>k</sub>]T_ is a |
| well-bounded type. |
| |
| *In short, a parameterized type based on a type alias, `F<...>`, must pass the |
| super-boundedness checks in itself, and so must the body of `F`.* |
| |
| *For instance, assume that `F` and `G` are declared as follows:* |
| ```dart |
| class A<X extends C<X>> { |
| ... |
| } |
| |
| typedef F<X extends C<X>> = A<X> Function(); |
| typedef G<X extends C<X>> = void Function(A<X>); |
| ``` |
| *The type `F<Object>` is then a super-bounded type, because `F<Null>` is |
| regular-bounded (`Null` is a subtype of `C<Null>`) and because |
| `A<Object> Function()` is well-bounded, because `A<Object>` is |
| super-bounded. Similarly, `G<Object>` is a super-bounded type because |
| `void Function(A<Object>)` is well-bounded because `A<Object>` is |
| super-bounded.* |
| |
| *Note that it is necessary to require that the right hand side of a type |
| alias declaration is taken into account when determining that a given |
| application of a type alias to an actual type argument list is correctly |
| super-bounded. That is, we do not think that it is possible for a |
| (reasonable) constraint specification mechanism on the formal type |
| parameters of a type alias declaration to ensure that all arguments |
| satisfying those constraints will also be suitable for the type on the |
| right hand side. In particular, we may use simple upper bounds and |
| F-bounded constraints (as we have always done), perform and pass the |
| 'correctly super-bounded' check on a given parameterized type based on a |
| type alias, and still have a right hand side which is not well-bounded:* |
| ```dart |
| class B<X extends List<num>> {} |
| typedef H<Y extends num> = void Function(B<List<Y>>); |
| typedef K<Y extends num> = B<List<Y>> Function(B<List<Y>>); |
| |
| H<Object> myH = null; // Error! |
| ``` |
| *`H<Object>` is a compile-time error because it is not regular-bounded |
| (`Object <: num` does not hold), and it is also not correctly |
| super-bounded: `Null` does satisfy the constraint in the declaration of |
| `Y`, but `H<Object>` gives rise to the right hand side |
| `void Function(B<List<Object>>)`, and that is not a well-bounded type: |
| It is not regular-bounded (`List<Object> <: List<num>` does not hold), |
| and it does not become a regular-bounded type by the type replacements |
| (that yield `void Function(B<List<Object>>)` because that occurrence of |
| `Object` is contravariant).* |
| |
| *Semantically, this failure may be motivated by the fact that `H<Object>`, |
| were it allowed, would not be a supertype of `H<T>` for all the `T` where |
| `H<T>` is regular-bounded. So it would not be capable of playing the role |
| as a "default type" that abstracts over all the possible actual types that |
| are expressible using `H`. For example, a variable declared like |
| `List<H<Object>> x;` would not be allowed to hold a value of type |
| `List<H<num>>` because the latter is not a subtype of the former.* |
| |
| *In the given situation it is possible to express such a default type: |
| `H<Null>` is actually a common supertype of `H<T>` for all `T` such that |
| `H<T>` is regular-bounded. However, `K` shows that this is not always the |
| case: There is no type `S` such that `K<S>` is a common supertype of `K<T>` |
| for all those `T` where `K<T>` is regular-bounded. Facing this situation, |
| we prefer to bail out rather than implicitly allow some kind of |
| super-bounded type (assuming that we amend the rules such that it is not an |
| error) which would not abstract over all possible instantiations anyway.* |
| |
| *The subtype relations for super-bounded types follow directly from the |
| extension of generic covariance to include actual type arguments that |
| violate the declared bounds. For the example in the Motivation section, `D` |
| is a subtype of `C<D>` which is a subtype of `C<C<D>>`, which is a subtype |
| of `C<C<C<D>>>`, continuing with `C<C<C<Object>>>>`, `C<C<Object>>`, |
| `C<Object>`, and `Object`, respectively, and similarly for `dynamic` and |
| `void`.* |
| |
| Types of members from super-bounded class types are computed using the same |
| rules as types of members from other types. Types of function applications |
| involving super-bounded types are computed using the same rules as types of |
| function applications involving other types. |
| |
| *For instance, using the example class `C` again, if `c1` has static type |
| `C<C<dynamic>>` then `c1.next` has static type `C<dynamic>` and |
| `c1.next.next` has static type `dynamic`. Similarly, if `List<X> foo(X)` |
| were the signature of a method in `C`, `c1.foo` would have static type |
| `List<C<dynamic>> Function(C<dynamic>)`. Note that the argument type `X` |
| makes that parameter of `foo` covariant, which implies that the reified |
| type of the tear-off `c1.foo` would have argument type `Object`, which |
| ensures that the expression `c1.foo` evaluates to a value whose dynamic |
| type is a subtype of the static type, as it should.* |
| |
| *Similarly, if we invoke an instance method with statically known argument |
| type `C<void>` whose argument is covariant, there will be a dynamic type |
| check on the actual argument (which might require that it is, say, of type |
| `D`); that check may fail at run time, but this is no different from the |
| situation with types that are not super-bounded. In general, the |
| introduction of super-bounded types does not introduce new soundness |
| considerations around covariance.* |
| |
| *Super-bounded function types do not have to be only in the statically |
| known types of first class functions, they can also be part of the actual |
| type of a function at run time. For instance, a function may be declared |
| as follows:* |
| |
| ```dart |
| List<C<dynamic>> foo(C<dynamic> x) { |
| ... |
| } |
| ``` |
| |
| *It would then have type exactly `List<C<dynamic>> Function(C<dynamic>)`, |
| and this means that it will accept an object which is an instance of a |
| subtype of `C<T>` for any `T`, and it will return a list whose element type |
| is some subtype of `C<dynamic>`, which could be `D` or `C<C<D>>` at run |
| time.* |
| |
| |
| ## Dynamic semantics |
| |
| The reification of a super-bounded type (*e.g., as a parameter type in a |
| reified function type*) uses the types as specified. |
| |
| *For instance `void foo(C<Object> x) => print(x);` will have reified type |
| `void Function(C<Object>)`. It is allowed for a run-time entity to have a |
| type which contains a super-bounded type, it is only prohibited for |
| run-time entities to have a super-bounded type themselves. So there can be |
| an instance whose dynamic type is `List<C<Object>>` but no instance whose |
| dynamic type is `C<Object>`.* |
| |
| The subtype rules used for run-time type tests, casts, and generated type |
| checks are the same as the subtype rules used during static analysis. |
| |
| *If an implementation applies an optimization that is only valid when |
| super-bounded types cannot exist, or in other ways relies on the (no longer |
| valid) assumption that super-bounded types cannot exist, it will need to |
| stop using that optimization or making that assumption. We do not expect |
| this to be a common situation, nor do we expect significant losses in |
| performance due to the introduction of this feature.* |
| |
| |
| ## Discussion |
| |
| The super-bounded type feature is all about violating bounds, in a |
| controlled manner. But what is the **motivation for enforcing bounds** in |
| the first place? The answer to that question serves to justify why it must |
| be 'controlled'. We have at least two reasons, one internal and one |
| external. |
| |
| The **internal reason** is that the bound of each formal type parameter is |
| relied upon during type checking of the body of the corresponding generic |
| declaration. For instance: |
| ```dart |
| class C<X extends num> { |
| X x; |
| bool get foo => x.isNegative; // Statically safe invocation. |
| } |
| ``` |
| If we ever allow an instance of `C<Object>` to be created, or even an |
| instance of a subclass which has `C<Object>` as a (possibly indirect) |
| superclass, then we could end up executing that implementation of `foo` in |
| a situation where `x` does not have an `isNegative` getter. In other words, |
| the internal issue is that super-bounding may induce a plain soundness |
| violation in the scope of the type parameter. |
| |
| This motivates the ban on super-bounding in instance creation expressions, |
| e.g., the ban on `new C<Object>()`. |
| |
| However, it does not suffice to justify banning super-bounded `implements` |
| clauses: There will not be any inherited method implementations from a type |
| that a given class implements, and hence no code will ever be executed in |
| the above situation (where a formal type parameter is in scope, and its |
| actual value violates the bound). In fact, code which could be executed in |
| this context would have static knowledge of the super-bound, and hence |
| there is no soundness issue in the body of such a class, nor in its |
| subclasses or subtypes. |
| |
| ```dart |
| // A thought experiment (explaining why this is a compile-time error). |
| class D implements C<Object> { |
| Object x; |
| bool get foo => false; |
| } |
| ``` |
| |
| It is reasonable to expect a `C<Object>` to have a field `x` of type |
| `Object` and a `foo` getter of type `bool`, and we can easily implement |
| that. There is no soundness issue, because no code is inherited from `C`. |
| |
| But there is also an **external reason**: It is reasonable to expect that |
| every instance will satisfy declared bounds, e.g., whenever an object is |
| accessed under the type `C<T>` for any `T`, it should be true that `T` is a |
| subtype of `num`. This is not a soundness issue per se; the class `D` is |
| perfectly consistent in its behavior with a typing as `C<Object>`, and its |
| implementation is type safe. |
| |
| However, it seems reasonable for developers to reckon as follows: When an |
| object _o_ has a static type like `C<Object>` it must satisfy the |
| expectations associated with `C`. So there exists an actual type argument |
| `T` which satisfies the declared bound, and _o_ must then behave like an |
| instance of `C<T>`. In the example, with the given bound `num` and using |
| covariance, _o_ would then be guaranteed to be typable as a `C<num>`. So |
| the following contains downcasts, but it is "reasonable" to expect them to |
| be guaranteed to succeed at run time: |
| |
| ```dart |
| C<Object> cObject = ...; // Complex creation. |
| C<num> cNum = cObject; // Safe, right? |
| bool b = (cObject.x as num).isNegative; // Also safe, right? |
| ``` |
| |
| If `D` is allowed to exist then we can have a consistent language, and the |
| above would be OK, but the "safe" downcasts would in fact fail at run |
| time. The point is that when we know something is a `C<Object>` then we know |
| that it satisfies the constraints of `C<Object>`, and we can't assume that |
| it satisfies any stronger constraints (such as those of `C<num>`). |
| |
| This is not a soundness issue in the traditional sense, but it is an issue |
| about how important it is to **allow** developers to make that **extra |
| assumption** that all implementations of a given generic class _G_ must be |
| just as picky about their actual type arguments as _G_ itself. |
| |
| We think that it is indeed justified to make these extra assumptions, and |
| **hence** we have **banned super-bounded `implements` clauses**. |
| |
| The extra assumptions which are now supported could be stated as: We can |
| rely on the declared bounds on type parameters of generic classes and |
| functions, also for code which is outside the scope of those type |
| parameters. |
| |
| In short, the underlying principle is that "there cannot be an instance of |
| a generic class (including instances of subtypes), nor an invocation of a |
| generic function, whose actual type arguments violate the declared |
| bounds". |
| |
| Super-bounded function types are possible and useful. Consider the |
| following example: |
| ```dart |
| // If bound on `X` holds then `C<X>` is regular-bounded. |
| typedef F<X extends C<X>> = C<X> Function(); |
| |
| main() { |
| F<C<dynamic>> f = ...; // OK, checking `F<C<Null>>` and `C<dynamic> Function()`. |
| var c0 = f(); // `c0` has type `C<C<dynamic>>`. |
| var c1 = c0.next; // `c1` has type `C<dynamic>` |
| var c2 = c1.next; // `c2` has type `dynamic` |
| ... |
| } |
| ``` |
| In this example, an unfolding of `C` to a specific level is supported in a |
| function type, and application of such a function immediately brings out |
| class types like `C<C<dynamic>>` that we have already argued are useful. |
| |
| |
| ## Updates |
| |
| * Version 0.8 (2018-10-16), emphasized that this document is no longer |
| specifying the current rules, it is for background info only. |
| |
| * Version 0.7 (2018-06-01), marked as background material: The normative |
| text on variance and on super-bounded types is now part of the language |
| specification. |
| |
| * Version 0.6 (2018-05-25), added example showing why we must check the |
| right hand side of type aliases. |
| |
| * Version 0.5 (2018-01-11), generalized to allow replacement of top types |
| covariantly and bottom types contravariantly. Introduced checks on |
| parameterized type aliases (such that bounds declared for the type |
| alias itself are taken into account). |
| |
| * Version 0.4 (2017-12-14), clarified several points and corrected |
| locations where super-bounded types were prohibited, but we should just |
| say that the bounds must be satisfied. |
| |
| * Version 0.3 (2017-11-07), dropping `super`, instead allowing `Object`, |
| `dynamic`, or `void` for super-bounded types, with a similar treatment as |
| `super` used to get. |
| |
| * Version 0.2 (2017-10-31), introduced keyword `super` as a type argument. |
| |
| * Version 0.1 (2017-10-20), initial version of this informal specification. |