-
Notifications
You must be signed in to change notification settings - Fork 217
Description
In response to #169, this is a proposal for a mechanism called type patterns. A type pattern P may contain certain syntactic elements that introduce a new type variable (look for var
), and it supports a check for whether a given type matches the pattern P. It is needed for various kinds of class extensions / extension methods under consideration.
The following spells out what a type pattern is and how it works, to a level of detail which is intended to support the assumption that the concept can be given a precise definition and that usages of this concept can be sound.
Examples
The basic construct defined in this proposal is a type patterns clause, which is a comma separated list of type patterns. Here are some examples, one per line, along with some comments giving hints about the meaning of the given construct.
// Match a single, fixed type with a type pattern.
int
List<int>
void Function()
// Match several fixed types with a type patterns clause.
A, B
Iterable<int>, List<dynamic>
// Match any type, and bind `X` to it.
var X
// Match any type `T` which satisfies `T <: C<T>`, and bind `X` to it.
var X extends C<X>
// Match any type of the form `Iterable<T>` and bind `X` to `T`;
// also, e.g., match `List<T>` and bind `X` to `T`.
Iterable<var X>
// Match any type of the form `T Function()` and bind `X` to `T`;
// also, e.g., match `T function([int])` and bind `X` to `T`.
var X Function()
// Match any type of the form `T Function(S, [int])`,
// binding `X` to `T` and `Y` to `S`;
// also, e.g., match `num Function(double, [num, num])`,
// binding `X` to `num` and `Y` to `double`.
var X Function(var Y, [int])
// Match `List<int>`, binding `X` to `int`; do not match `List<String>`.
Iterable<var X extends num>
// Error: cannot introduce two type variables with the same name
// in one type patterns clause.
var X, List<var X>
// Match `Map<num, num>` and bind `X` to `num`;
// match `Map<num, int>` and bind `X` to `num`;
// do not match `Map<int, num>`.
Map<var X, X>
// Error if subtype robustness required (e.g., for extension methods):
// Cannot declare a match bound in a contravariant position.
extension A on void Function(var X extends num) { ... }
First note that every type is also a type pattern, so it is always possible to use any particular type as a pattern. The intuition behind this is that it matches the specified type.
Next, when a type is a subtype of a type pattern which is also derivable from <type>
, it also matches. So int
matches num
, and all types match dynamic
.
Finally, a type patterns clause containing primitive type patterns (that is, where var
occurs) matches the types as described in the comments above, which generally means that T
matches P
when it is possible to extract a value for each type variable introduced by a primitive type pattern in P
which satisfies the bounds, if any, and then T
matches the result.
Syntax
This proposal extends the Dart grammar with the following rules:
<typePatterns> ::= <typePattern> (',' <typePattern>)*
<typePattern> ::= <functionTypePatternTails>
| <typePatternNotFunction> <functionTypePatternTails>
| <typePatternNotFunction>
<functionTypePatternTails> ::= <functionTypePatternTail> <functionTypePatternTails>
| <functionTypePatternTail>
<typePatternNotFunction> ::= <typePatternNotVoidNotFunction>
| 'void'
<functionTypePatternTail> ::=
'Function' <typePatternParameters>? <parameterTypePatternList>
<typePatternNotVoidNotFunction> ::= <typeName> <typePatternArguments>?
| 'Function'
| 'var' <typeIdentifier> ('extends' <type>)?
<typePatternParameters> ::=_T<sub>j</sub>
'<' <typePatternParameter> (',' <typePatternParameter>)* '>'
<parameterTypePatternList> ::= '(' ')'
| '(' <normalParameterTypePatterns> ',' <optionalParameterTypePatterns> ')'
| '(' <normalParameterTypePatterns> ','? ')'
| '(' <optionalParameterTypePatterns> ')'
<typePatternArguments> ::= '<' <typePatternList> '>'
<typePatternParameter> ::=
<metadata> <typeIdentifier> ('extends' <typePatternNotVoid>)?
<normalParameterTypePatterns> ::=
<normalParameterTypePattern> (',' <normalParameterTypePattern>)*
<optionalParameterTypePatterns> ::= <optionalPositionalParameterTypePatterns>
| <namedParameterTypePatterns>
<typePatternList> ::= <typePattern> (',' <typePattern>)*
<typePatternNotVoid> ::= <functionTypePattern>
| <typePatternNotVoidNotFunction>
<normalParameterTypePattern> ::= <patternTypedIdentifier>
| <typePattern>
<optionalPositionalParameterTypePatterns> ::=
'[' <normalParameterTypePatterns> ','? ']'
<namedParameterTypePatterns> ::=
'{' <patternTypedIdentifier> (',' <patternTypedIdentifier>)* ','? '}'
<functionTypePattern> ::= <functionTypePatternTails>
| <typePatternNotFunction> <functionTypePatternTails>
<patternTypedIdentifier> ::= <typePattern> <identifier>
We use the phrase type patterns clause for terms derived from <typePatterns>
, and type pattern for terms derived from <typePattern>
.
Note that there is currently no way in the Dart grammar to use a type pattern or a type patterns clause; it is up to other extensions of Dart to introduce syntactic locations where these constructs can occur.
Static Analysis
Every occurrence of a type alias F<T1, .. Tk> in a pattern is replaced by the expansion [T1/X1, .. Tk/Xk]B, where B is the body of F and Xj, 1 <= j <= k are the type parameters of F. A non-generic type alias is covered by the case k = 0.
Let Ps be a type patterns clause. For any <typeIdentifier>
X, it is a compile-time error if Ps contains two or more primitive type patterns introducing X. Otherwise we say that Ps introduces the set of type variables that are introduced by primitive type patterns in Ps.
The variance of a position in a pattern is declared in the same way as for positions in a type. It is a compile-time error if a primitive type pattern with a bound occurs in a contravariant position. It is a compile-time error if a primitive type pattern occurs in an invariant position. (We could make the latter a syntax error, but then the expansion of type aliases could introduce syntax errors, so it's simpler to allow the syntax and make it a non-syntax compile-time error.)
Let Ps of the form P1, .. Pk be a type patterns clause that introduces the type variable X in Pj for some j in 1..k; it may also introduce other type variables in any of its type patterns. Let T be a type. The type S is the type that corresponds to X in Ps if and only if S is the type that corresponds to X in Pj.
The notion of a type that corresponds to a type variable introduced by a type pattern is defined in terms of the rules stated below. We need the special placeholders Up
and Down
which will be used during the computation of corresponding types, but which will never occur in the resulting bindings.
Consider the situation where we wish to determine whether a type pattern P
matches a given type T
. The first step taken is to substitute Down
for every covariant occurrence of Null
in T
, and Up
for every contravariant occurrence of Null
in T
, yielding the term t
. In the following we will consider Up
and Down
as names of types. (So Iterable<Up>
is a superinterface of List<Up>
, etc.) Subsequent steps are specified in the following rules:
Let P
be a type pattern of the form var X
or var X extends S
. In this case X
corresponds to Object
with respect to (P
, Up
) (note that the bound cannot exist in this case because P
occurs in a contravariant position), and X
corresponds to Null
with respect to (P
, Down
). Let P
be a type pattern of the form var X
or var X extends S
, and let t
be a term different from Up
and Down
. In this case X
corresponds to [Null/Up, Null/Down]t
with respect to (P
, t
).
Let P
be a type pattern of the form C<P1, .., Pj, .., Pn>
where Pj
is a type pattern that introduces X
. In this case X
corresponds to U
with respect to (P
, Up
) if X
corresponds to U
with respect to (Pj
, Up
); X
corresponds to U
with respect to (P
, Down
) if X
corresponds to U
with respect to (Pj
, Down
).
Let P
be a type pattern of the form C<P1, .., Pj, .., Pn>
where Pj
is a type pattern that introduces X
, and let t
be a term which has a direct or indirect superinterface of the form C<t1, .., tj, .. tn>
(note that t
then cannot be Null
, Up
, or Down
). In this case X
corresponds to U
with respect to (P
, t
) if X
corresponds to U
with respect to (Pj
, tj
).
Note that the match cannot be with respect to (P
, Null
), because this implies that the original pattern had an invariant occurrence of a primitive pattern introducing X
, and that's an error. The same argument applies in the following cases, which is the reason why matching with respect to (P
, Null
) is not mentioned.
Let P
be a type pattern of the form P0 Function<...>(...)
where P0
is a type pattern that introduces X
. In this case X
corresponds to U
with respect to (P
, Up
) if X
corresponds to U
with respect to (P0
, Up
), and X
corresponds to U
with respect to (P
, Down
) if X
corresponds to U
with respect to (P0
, Down
). Let P
be a type pattern of the form P0 Function<...>(...)
where P0
is a type pattern that introduces X
, and let t
be a term of the form s Function<...>(...)
. In this case X
corresponds to U
with respect to (P
, t
) if X
corresponds to U
with respect to (P0
, s
). In all cases, matching has failed if the corresponding type has a free occurrence of one of Y1..Ys
.
Note that non-generic function types are handled by the special case where <...>
declares zero type parameters, in which case it is omitted. We do not require that the elided parts of the function types have any particular relationships with each other, because we will apply a subtype check afterwards in order to ensure that matching only succeeds when the types are appropriately related. Similar considerations apply for similar situations below.
Let P
be a type pattern of the form P0 Function<Y1 extends B1, .. Ys extends Bs>(..., Pj, ...)
where Pj
is a type pattern that introduces X
. In this case X
corresponds to U
with respect to (P
, Up
) if X
corresponds to U
with respect to (Pj
, Down
), and X
corresponds to U
with respect to (P
, Down
) if X
corresponds to U
with respect to (Pj
, Up
). In all cases, matching has failed if the corresponding type has a free occurrence of one of Y1..Ys
.
Let P
be a type pattern of the form P0 Function<Y1 extends B1, .. Ys extends Bs>(..., Pj, ...)
where Pj
is a type pattern that introduces X
, and let t
be a term of the form t0 Function<Y1 extends B1, .. Ys extends Bs>(..., tj, ...)
. In this case X
corresponds to U
with respect to (P
, t
) if X
corresponds to U
with respect to (Pj
, tj
). In all cases, matching has failed if the corresponding type has a free occurrence of one of Y1..Ys
.
(TODO: Spell out treatment of optional parameters for all forms of function type. Double-check the approach to generic function types.)
Let Ps of the form P1, .. Pk be a type patterns clause introducing type variables X1 .. Xs, and T a type. T matches Ps if and only if there exist types T1 .. Ts such that, for each j in 1..s, Tj satisfies its bound, if any, and Xj corresponds to Tj with respect to (Ps, T), and T is a subtype of [T1/X1 .. Ts/Xs]Pj for each j in 1..k.
Note that "exist types" does not imply that a costly search must be performed: Each step in the matching algorithm proceeds with a subterm of the given types being matched up (except for the super-interface step, where we may need to traverse the whole superinterface graph in order to find a type which is sufficiently similar to the pattern that it is being matched against), and then type variables are bound basically by being "looked up". If that process succeeds then we have performed an amount of work that is similar to a subtype check. If it fails then such types do not exist.
Subtyping Property
We say that a type patterns clause Ps is subtype robust when none of the type variables introduced by Ps occur except in the type pattern that introduces it, no primitive type pattern occurring in a contravariant position has a bound, and no primitive type pattern occurs as the return type or as a parameter type of a generic function type.
For example, var X
and Map<var X, var Y>
are subtype robust. But Map<var X, X>
is not, because X
occurs twice; this destroys subtype robustness because we could match statically with Map<num, num>
, binding X
to num
statically, and the run-time type could then be Map<int, num>
, which is a subtype of Map<num, num>
but which fails to match Map<var X, X>
.
Similarly, A<var X>, B<var Y extends X>
is not subtype robust because X
occurs twice; this destroys subtype robustness because the run-time type could implement A<int>
and B<num>
(which means that matching will fail), but the static type could still have A<num>
and B<num>
as superinterfaces.
Finally, Function(var X extends num)
is not subtype robust because X
has a bound and occurs contravariantly. This breaks subtype robustness because the static type could be Function(num)
and the run-time type could be Function(Object)
, and this fails to match because X <: num
would be violated.
We consider the following property to be likely provable: Assume that Ps is a subtype robust type patterns clause and T is a type such that Ps matches T, binding its type variables X1 .. Xs to types T1 .. Ts; assume that S is a subtype of T; then Ps matches S, binding its type variables to S1 .. Ss, and for each j it is guaranteed that if Xj occurs covariantly in Ps then Sj <: Tj, if Xj occurs contravariantly in Ps then Tj <: Sj. (It is a compile-time error of Ps if Xj occurs invariantly.)
Discussion
Subtype robust type patterns clauses are particularly useful with static extension methods, because we want to ensure that a subtype will successfully match a type patterns clause whenever a supertype is known to do so, because this means that we can safely match against the dynamic type in order to obtain access to the actual values of type arguments on the receiver type.
Type patterns that are not subtype robust are particularly attractive with dynamic tests. For instance, a test that x is Map<var X, var Y extends X>
will only match when the dynamic type of the value of x
is a subtype of Map<exactly S, exactly T>
such that T <: S
. This means, for instance, that we can use values from this map as keys in the same map. The ability to establish such type relationships is a significant expansion of the expressive power of Dart.
The special treatment of Null
(and, in the future, an explicit bottom type would get a similar treatment) ensures that subtypes containing Null
will have well-defined bindings for all type variables introduced by a type patterns clause. Otherwise, the pattern List<List<var X>>
could soundly bind X
to any type when matched with List<Null>
.
In some sense that choice may seem to be unimportant, because any actual elements of the list well be the null object, so there is no way to disprove that "if this element had been a list, it's type argument would be T
", for any T
, but the current specification ensures that the chosen binding of X
in the example will be such that the matched type (List<Null>
) is "as close as possible" to the matching type (List<List<Null>>
, based on the pattern and the binding of X
to Null
), which means that whenever a type S
matches the pattern and is a supertype of the matched type, it is also a supertype of the matching type. This means that an extension method may be invoked on a receiver of type List<Null>
whose static type is List<List<T>>
for some T
, and that method may create and return a List<List<Null>>
based on the binding of X
, and the returned list will then actually have the type List<List<T>>
, no matter which T
it is.
The notion of a 'corresponding type' for a type variable with respect to a pattern and a type has a simple intuitive interpretation: We simply "look up the actual type argument at the same position as the given type variable in the pattern".
The example where the pattern Map<var X, X>
will match Map<num, int>
and bind X
to num
shows that this allows for a certain flexibility. But the pattern Map<X, var X>
with the same type illustrates that it is possible to select a value for X
(namely num
) such that the given type Map<num, int>
is a subtype of the type Map<num, num>
produced by the match. Still, the match fails because it only considers binding X
to int
. This example illustrates why it makes sense to think about the occurrence of X
which does not have the marker var
as a "constraint".
It would also be possible to use a pure constraint based approach where all occurrences of X
are treated as potential sources of information about possible values of X
(as constraints on X
). This would be more powerful than the approach that we have actually chosen, but also considerably more complex, presumably both in the implementation and when reasoning about what it does.
We believe that the chosen approach is a useful trade-off between expressive power and complexity, because it helps keeping the complexity of the matching operation low, and it keeps the results more predictable, especially in the case where an interface type has a type argument which is used in a contravariant location in a member signature.