Added informal specification of super-bounded types.
Change-Id: I4764faa8250af61bf8cda9cf6a854e2bd7d726f2 Reviewed-on: https://dart-review.googlesource.com/14042 Reviewed-by: Leaf Petersen <leafp@google.com> Reviewed-by: Lasse R.H. Nielsen <lrn@google.com>
This commit is contained in:
@@ -1,61 +1,70 @@
|
||||
## Feature: Instantiate to Bound
|
||||
|
||||
Author: eernst@
|
||||
**Author**: eernst@
|
||||
|
||||
**Version**: 0.5 (2018-01-11)
|
||||
|
||||
**Status**: Under implementation.
|
||||
|
||||
Based on [this description](https://github.com/dart-lang/sdk/issues/27526#issuecomment-260021397) by leafp@.
|
||||
|
||||
**Status**: Under implementation. Remaining issue: Can a super-bounded
|
||||
type be created by instantiate to bound?
|
||||
|
||||
**This document** is an informal specification of the the instantiate to bound
|
||||
mechanism in Dart 2. The feature described here, *instantiate to bound*, makes
|
||||
it possible to omit some or all actual type arguments in some types using
|
||||
generic classes. The missing type arguments will be added implicitly, and the
|
||||
chosen value for a given type argument will be the bound on the corresponding
|
||||
formal type parameter. In some situations no such bound can be expressed, in
|
||||
which case a compile-time error occurs. To resolve that, the type arguments
|
||||
can be given explicitly.
|
||||
**This document** is an informal specification of the the instantiate to
|
||||
bound mechanism in Dart 2. The feature described here, *instantiate to
|
||||
bound*, makes it possible to omit some or all actual type arguments in some
|
||||
types using generic classes. The missing type arguments will be added
|
||||
implicitly, and the chosen value for a given type argument will be the
|
||||
bound on the corresponding formal type parameter. In some situations no
|
||||
such bound can be expressed, in which case a compile-time error occurs. To
|
||||
resolve that, the type arguments can be given explicitly.
|
||||
|
||||
## Background
|
||||
|
||||
In Dart 1.x, missing actual type arguments were filled in with the value
|
||||
`dynamic`, which was always a useful choice because that would yield types
|
||||
which were at the bottom of the set of similar types, as well as at the top.
|
||||
which were at the bottom of the set of similar types, as well as at the
|
||||
top.
|
||||
|
||||
```dart
|
||||
List xs = <int>[]; // OK, also dynamically: List<int> <: List<dynamic>.
|
||||
List<int> y = new List(); // OK, also dynamically: List<dynamic> <: List<int>.
|
||||
```
|
||||
|
||||
In Dart 2, type inference is used in many situations to infer missing type
|
||||
arguments, hence selecting values that will work in the given context.
|
||||
However, when the context does not provide any information to this inference
|
||||
process, some default choice must be made.
|
||||
However, when the context does not provide any information to this
|
||||
inference process, some default choice must be made.
|
||||
|
||||
In Dart 2, `dynamic` is no longer a super- and subtype of all other types,
|
||||
and hence using `dynamic` as the default value for a missing actual
|
||||
type argument will create many malformed types:
|
||||
and hence using `dynamic` as the default value for a missing actual type
|
||||
argument will create many malformed types:
|
||||
|
||||
```dart
|
||||
class A<X extends num> {}
|
||||
A a = null; // A is malformed if interpreted as A<dynamic>.
|
||||
```
|
||||
Hence, a new rule for finding default actual type arguments must be specified.
|
||||
|
||||
Hence, a new rule for finding default actual type arguments must be
|
||||
specified.
|
||||
|
||||
## Motivation
|
||||
|
||||
It is convenient for developers to be able to use a more concise notation for
|
||||
some types, and instantiate to bound will enable this.
|
||||
It is convenient for developers to be able to use a more concise notation
|
||||
for some types, and instantiate to bound will enable this.
|
||||
|
||||
We will use a relatively simple mechanism which is allowed to fail. This means
|
||||
that developers will have to write actual type arguments explicitly in some
|
||||
ambiguous situations, thus adding visual complexity to the source code and
|
||||
requiring extra time and effort to choose and write those arguments. However,
|
||||
we consider the ability to reason straightforwardly about generic types in
|
||||
general more important than (possibly misleading) conciseness.
|
||||
We will use a relatively simple mechanism which is allowed to fail. This
|
||||
means that developers will have to write actual type arguments explicitly
|
||||
in some ambiguous situations, thus adding visual complexity to the source
|
||||
code and requiring extra time and effort to choose and write those
|
||||
arguments. However, we consider the ability to reason straightforwardly
|
||||
about generic types in general more important than (possibly misleading)
|
||||
conciseness.
|
||||
|
||||
The performance characteristics of the chosen algorithm plays a role as well,
|
||||
because it is important to be able to find default type arguments in a short
|
||||
amount of time. Because of that, we have chosen to require explicit type
|
||||
arguments on bounds except for some "simple" cases. Again, this means that the
|
||||
source code will be somewhat more verbose, in return for overall simplicity.
|
||||
The performance characteristics of the chosen algorithm plays a role as
|
||||
well, because it is important to be able to find default type arguments in
|
||||
a short amount of time. Because of that, we have chosen to require explicit
|
||||
type arguments on bounds except for some "simple" cases. Again, this means
|
||||
that the source code will be somewhat more verbose, in return for overall
|
||||
simplicity.
|
||||
|
||||
Here are some examples:
|
||||
|
||||
@@ -75,7 +84,7 @@ C x;
|
||||
|
||||
class D<T extends Comparable<T>> {}
|
||||
|
||||
// Error: no default type arguments can be computed for D.
|
||||
// The raw type D is completed to D<Comparable<dynamic>>.
|
||||
D x;
|
||||
|
||||
// Error: T of D does not have a simple bound, so raw D cannot be a bound.
|
||||
@@ -91,21 +100,24 @@ This mechanism does not require any grammar modifications.
|
||||
## Static analysis
|
||||
|
||||
Let _G_ be a generic class with formal type parameter declarations
|
||||
_F1 .. Fk_ containing formal type parameters _X1 .. Xk_ and bounds _B1
|
||||
.. Bk_. We say that the formal type parameter _Xj_ has a _simple bound_
|
||||
when one of the following requirements is satisfied:
|
||||
_F<sub>1</sub> .. F<sub>k</sub>_ containing formal type parameters
|
||||
_X<sub>1</sub> .. X<sub>k</sub>_ and bounds
|
||||
_B<sub>1</sub> .. B<sub>k</sub>_. We say that the formal type parameter
|
||||
_X<sub>j</sub>_ has a _simple bound_ when one of the following requirements
|
||||
is satisfied:
|
||||
|
||||
1. _Bj_ is omitted.
|
||||
1. _B<sub>j</sub>_ is omitted.
|
||||
|
||||
2. _Bj_ is included, but does not contain any of _X1 .. Xk_. If _Bj_
|
||||
contains a type _T_ on the form `qualified` (*for instance, `C` or
|
||||
`p.D`*) which denotes a generic class _G1_ (*that is, _T_ is a raw
|
||||
type*), every type argument of _G1_ has a simple bound.
|
||||
2. _B<sub>j</sub>_ is included, but does not contain any of _X<sub>1</sub>
|
||||
.. X<sub>k</sub>_. If _B<sub>j</sub>_ contains a type _T_ on the form
|
||||
`qualified` (*for instance, `C` or `p.D`*) which denotes a generic class
|
||||
_G<sub>1</sub>_ (*that is, _T_ is a raw type*), every type argument of
|
||||
_G<sub>1</sub>_ has a simple bound.
|
||||
|
||||
The notion of a simple bound must be interpreted inductively rather
|
||||
than coinductively, i.e., if a bound _Bj_ of a generic class _G_ is
|
||||
reached during an investigation of whether _Bj_ is a simple bound,
|
||||
the answer is no.
|
||||
The notion of a simple bound must be interpreted inductively rather than
|
||||
coinductively, i.e., if a bound _B<sub>j</sub>_ of a generic class _G_ is
|
||||
reached during an investigation of whether _B<sub>j</sub>_ is a simple
|
||||
bound, the answer is no.
|
||||
|
||||
*For example, with `class C<X extends C> {}` the type parameter `X` does
|
||||
not have a simple bound.*
|
||||
@@ -114,10 +126,10 @@ not have a simple bound.*
|
||||
involved types to be "simple enough". We impose the following constraint on
|
||||
all bounds because any generic type may be used as a raw type.*
|
||||
|
||||
It is a compile-time error if a formal parameter bound _B_ contains
|
||||
a type _T_ on the form `qualified` and _T_ denotes a generic class
|
||||
_G_ (*that is, _T_ is a raw type*), unless every formal type parameter of
|
||||
_G_ has a simple bound.
|
||||
It is a compile-time error if a formal parameter bound _B_ contains a type
|
||||
_T_ on the form `qualified` and _T_ denotes a generic class _G_ (*that is,
|
||||
_T_ is a raw type*), unless every formal type parameter of _G_ has a simple
|
||||
bound.
|
||||
|
||||
*In short, type arguments on bounds can only be omitted if they themselves
|
||||
have simple bounds. In particular, `class C<X extends C> {}` is a
|
||||
@@ -141,56 +153,94 @@ it will not make the instantiate to bound feature obsolete: instatiate to
|
||||
bound would still be used in cases where no information is available to
|
||||
infer the omitted type arguments, e.g., for `List xs = [];`.*
|
||||
|
||||
When type inference is providing actual type arguments for a term _G_ on
|
||||
*When type inference is providing actual type arguments for a term _G_ on
|
||||
the form `qualified` which denotes a generic class, instantiate to bound
|
||||
will provide the value for a single type argument in cases where no
|
||||
information is available for inferring such an actual type argument. In
|
||||
this situation, it is a compile-time error if instantiate to bound fails.
|
||||
|
||||
In all these cases, instantiate to bound selects the value for an omitted
|
||||
actual type argument as follows:
|
||||
may be used to provide the value for type arguments where no information is
|
||||
available for inferring such an actual type argument. This document does
|
||||
not specify how inference interacts with instantiate to bound, that will be
|
||||
specified as part of the specification of inference. We will hence proceed
|
||||
to specify instantiate to bound as it applies to a type argument list which
|
||||
is omitted, such that a value for all the actual type arguments must be
|
||||
computed.*
|
||||
|
||||
Let _T_ be a `qualified` term which denotes a generic class _G_ (*so _T_ is
|
||||
a raw type*), let _F1 .. Fk_ be the formal type parameter declarations in the
|
||||
declaration of _G_, with type parameters _X1 .. Xk_ and bounds _B1 .. Bk_
|
||||
with types _T1 .. Tk_, and let _j_ be the position of the actual type
|
||||
argument for which the selection will be made.
|
||||
a raw type*), let _F<sub>1</sub> .. F<sub>k</sub>_ be the formal type
|
||||
parameter declarations in the declaration of _G_, with type parameters
|
||||
_X<sub>1</sub> .. X<sub>k</sub>_ and bounds _B<sub>1</sub>
|
||||
.. B<sub>k</sub>_ with types _T<sub>1</sub> .. T<sub>k</sub>_. For _i_ in
|
||||
_1 .. k_, let _S<sub>i</sub>_ denote the result of performing instantiate
|
||||
to bound on the type in the bound, _T<sub>i</sub>_; in the case where
|
||||
_B<sub>i</sub>_ is omitted, let _S<sub>i</sub>_ be `dynamic`.
|
||||
|
||||
If _Bj_ is omitted, the selected value for _Xj_ is `dynamic`.
|
||||
*Note that if _T<sub>i</sub>_ for some _i_ is raw then we know that all its
|
||||
omitted type arguments have simple bounds, which limits the complexity of
|
||||
the instantiate to bound step for _T<sub>i</sub>_.*
|
||||
|
||||
Otherwise, if _Bj_ does not contain any of the formal type parameters
|
||||
_X1 .. Xk_, the selected value for _Xj_ is the result of performing
|
||||
instantiate to bound on the type in the bound, _Tj_.
|
||||
Instantiate to bound then computes an actual type argument list for _G_ as
|
||||
follows:
|
||||
|
||||
*Note that if _Tj_ is raw then we know that all its omitted type arguments
|
||||
have simple bounds, which limits the complexity of the instantiate to bound
|
||||
step for _Tj_.*
|
||||
Let _U<sub>i,1</sub>_ be _S<sub>i</sub>_, for all _i_ in _1 .. k_. (*This
|
||||
is the "current value" of the bound for type variable _i_, at step 1; in
|
||||
general we will consider the current step, _m_, and use data for that step,
|
||||
e.g., the bound _U<sub>i,m</sub>_, to compute the data for step _m + 1_*).
|
||||
|
||||
Otherwise, define the substitution _s_ as follows: for all _j_ in _1 .. k_,
|
||||
replace _Xj_ by the result of applying instantiate to bound on _Tj_. Now,
|
||||
repeatedly apply _s_ on the types in the bounds _T1 .. Tk_ until _Tj_ does
|
||||
not contain any of the formal type parameters _X1 .. Xk_.
|
||||
Let _--><sub>m</sub>_ be a relation among the type variables
|
||||
_X<sub>1</sub> .. X<sub>k</sub>_ such that
|
||||
_X<sub>p</sub> --><sub>m</sub> X<sub>q</sub>_ iff _X<sub>q</sub>_ occurs in
|
||||
_U<sub>p,m</sub>_ (*so each type variable is related to, that is, depends
|
||||
on, every type variable in its bound, possibly including itself*).
|
||||
Let _==><sub>m</sub>_ be the transitive closure of _--><sub>m</sub>_.
|
||||
For each _m_, let _U<sub>i,m+1</sub>_, for _i_ in _1 .. k_, be determined
|
||||
as follows:
|
||||
|
||||
If this process terminates then the selected value is the final
|
||||
value of the bound in _Fj_. If it does not terminate, the instantiate
|
||||
to bounds process has failed.
|
||||
- Let _j_ be the lowest number such that
|
||||
_X<sub>j</sub> ==><sub>m</sub> X<sub>j</sub>_ (*that is, such that this
|
||||
type variable is a member of a dependency cycle*).
|
||||
Let _{Y<sub>1</sub> .. Y<sub>p</sub>}_ be the the maximal subset
|
||||
containing _X<sub>j</sub>_ of _{X<sub>1</sub> .. X<sub>k</sub>}_ where
|
||||
every pair is related: _Y<sub>q</sub> ==><sub>m</sub> Y<sub>r</sub>_, for
|
||||
all _q, r_ in _1 .. p_ (*i.e., this subset is a strongly connected
|
||||
component in the dependency graph containing _X<sub>j</sub>_*).
|
||||
Then _U<sub>i,m+1</sub>_ is the result of substituting, in
|
||||
_U<sub>i,m</sub>_, `Null` for every contravariant occurrence
|
||||
of _X<sub>j</sub>_, and `dynamic` for every covariant occurrence of
|
||||
_X<sub>j</sub>_, for all _i_ in _1 .. k_ such that _X<sub>i</sub>_ is a
|
||||
member of _{Y<sub>1</sub> .. Y<sub>p</sub>}_; and _U<sub>i,m+1</sub>_ is
|
||||
_U<sub>i,m</sub>_, for all other _i_ in _1 .. k_.
|
||||
*That is, we update the bounds of type variables in the dependency cycle
|
||||
such that the type variables in the dependency cycle are replaced by
|
||||
`Null` or `dynamic`, depending on the variance of the location.*
|
||||
|
||||
*It can always be determined whether the process will terminate, because
|
||||
it always replaces each formal type parameter by a specific term, thus
|
||||
incrementally building a regular tree: If the bound in _Fj_ at some step
|
||||
_s_ contains some type parameter _Xm_, and it contains _Xm_ again at a
|
||||
later step _s+n_, then the process will not terminate. If no such
|
||||
repeated occurrence of a type parameter occurs then the process will
|
||||
terminate, because the set of formal type parameters is finite.*
|
||||
- Otherwise, (*if no such dependency cycle exists*) let _j_ be the
|
||||
lowest number such that _X<sub>j</sub>_ occurs in
|
||||
_U<sub>p,m</sub>_ for some _p_ and
|
||||
_X<sub>j</sub> -/-><sub>m</sub> X<sub>q</sub>_ for all _q_ in _1..k_
|
||||
(*that is, _U<sub>j,m</sub>_ is closed, that is, the current bound of
|
||||
_X<sub>j</sub>_ does not depend on any other type variables; but
|
||||
_X<sub>j</sub>_ is being depended on by the bounds of some other type
|
||||
variables*). Then _U<sub>i,m+1</sub>_ is the result of substituting, in
|
||||
_U<sub>i,m</sub>_, `Null` for every contravariant occurrence
|
||||
of _X<sub>j</sub>_, and _U<sub>j,m</sub>_ for every covariant occurrence
|
||||
of _X<sub>j</sub>_, for all _i_ in _1 .. k_.
|
||||
|
||||
*Note that instantiate to bound will always fail if the bound
|
||||
on _Fj_ is an F-bound, e.g., `class A<T extends B<T>>`.*
|
||||
- Otherwise, (*when no dependencies exist*) terminate with the result
|
||||
_<U<sub>1,m</sub> ..., U<sub>k,m</sub>>_.
|
||||
|
||||
*This process will always terminate, because the total number of
|
||||
occurrences of type variables from _{X<sub>1</sub> .. X<sub>k</sub>}_ in
|
||||
the current bounds is strictly decreasing with each step, and we terminate
|
||||
when that number reaches zero.*
|
||||
|
||||
*Note that this process may produce a
|
||||
[super-bounded type](https://github.com/dart-lang/sdk/blob/master/docs/language/informal/super-bounded-types.md).*
|
||||
|
||||
When instantiate to bound is applied to a type it proceeds recursively: For
|
||||
a generic instantiation `G<T1..Tk>` it is applied to `T1..Tk`; for
|
||||
a function type `T0 Function(T1..Tj, {Tj+1 xj+1 .. Tk xk})` and
|
||||
a function type `T0 Function(T1..Tj, [Tj+1 .. Tk])` it is applied
|
||||
to `T0..Tk`.
|
||||
a generic instantiation _G<T<sub>1</sub> .. T<sub>k</sub>>_ it is applied
|
||||
to _T<sub>1</sub> .. T<sub>k</sub>_; for a function type
|
||||
_T<sub>0</sub> Function(T<sub>1</sub> .. T<sub>j</sub>, {T<sub>j+1</sub> x<sub>1</sub> .. T<sub>k</sub> x<sub>j+k</sub>})_
|
||||
and a function type
|
||||
_T<sub>0</sub> Function(T<sub>1</sub> .. T<sub>j</sub>, [T<sub>j+1</sub> .. T<sub>j+k</sub>])_
|
||||
it is applied to _T<sub>0</sub> .. T<sub>j+k</sub>_.
|
||||
|
||||
*This means that instantiate to bound has no effect on a type that does not
|
||||
contain any raw types; conversely, instantiate to bound will act on types
|
||||
@@ -199,34 +249,24 @@ which are syntactic subterms, no matter where they occur.*
|
||||
|
||||
## Dynamic semantics
|
||||
|
||||
There is no separate dynamic semantics for this mechanism. The semantics
|
||||
of a given program _P_ is the semantics of the program _P'_ which is
|
||||
created from _P_ by applying instantiate to bound where applicable.
|
||||
|
||||
|
||||
## Discussion
|
||||
|
||||
A more complex algorithm was considered, possibly involving support for
|
||||
recursive (infinite) types. For example:
|
||||
|
||||
```dart
|
||||
// A global dependency: F and G could be in different libraries.
|
||||
class F<T extends G> {}
|
||||
class G<T extends F> {}
|
||||
```
|
||||
|
||||
In this case, instantiating the bounds to the infinite terms
|
||||
`G<F<G<F<G<F<...>>>>>>` and `F<G<F<G<F<G<...>>>>>>` would be a consistent
|
||||
solution, which could be justified by means of a coinductive
|
||||
interpretation of what it means to be a 'simple bound'. However, we do not
|
||||
expect solutions to this kind of challenge to be sufficiently useful
|
||||
(if even possible) to justify the added complexity, both with respect to
|
||||
comprehensibility for human readers, and with respect to the performance
|
||||
of tools.
|
||||
The instantiate to bound transformation which is specified in the static
|
||||
analysis section is used to provide type arguments to dynamic invocations
|
||||
of generic functions, when no actual type arguments are passed. Otherwise,
|
||||
the semantics of a given program _P_ is the semantics of the program _P'_
|
||||
which is created from _P_ by applying instantiate to bound where
|
||||
applicable.
|
||||
|
||||
|
||||
## Updates
|
||||
|
||||
* Jan 11th 2018, version 0.5: Revised treatment of variance based on
|
||||
strongly connected components in the dependency graph.
|
||||
|
||||
* Dec 13th 2017: Revised to allow infinite substitution sequences when the
|
||||
value of a type argument is computed, specifying how to detect that
|
||||
the substitution sequence is infinite, and how to obtain a result from
|
||||
there.
|
||||
|
||||
* Sep 15th 2017: Transferred to the SDK repository as
|
||||
[instantiate-to-bound.md](https://github.com/dart-lang/sdk/blob/master/docs/language/informal/instantiate-to-bound.md).
|
||||
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
## Feature: Super-bounded Types
|
||||
|
||||
**Author**: eernst@.
|
||||
|
||||
**Version**: 0.5 (2018-01-11).
|
||||
|
||||
**Status**: Under discussion.
|
||||
|
||||
**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>>('Hello!')`, nor can the type
|
||||
argument be inferred to `List<dynamic>` in an invocation like
|
||||
`foo('Hello!')`. 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` at all without an explicit
|
||||
cast. 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.*
|
||||
|
||||
*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.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.
|
||||
Reference in New Issue
Block a user