-
Notifications
You must be signed in to change notification settings - Fork 205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Type Patterns #170
Comments
Yes, main() {
Object map = ...; // Assume non-empty.
if (map is Map<var X, X>) {
X x = map.values.first; // Safe (upcast).
map[x /*safe (same type)*/] = x; // Dynamic check on passing `x`.
}
if (map is Map<var X, var Y extends X>) {
Y y = map.values.first; // Safe (same type).
X x = y; // Safe (upcast).
map[y /*safe (upcast)*/] = y; // Safe to pass `y`.
}
} But they are similar in that they will both match exactly those types |
The runtime type can be Map<var X, X> map = <num, int>{1: 1}; This assignment should succeed, implying that the type pattern match was successful. |
@lrhn already mentioned an example where we'd allow type patterns as variable type annotations (which is not a case that I've considered, but we could do that and then put Just to make it fully explicit: Yes, However, it's also relevant to perform matching for a given static type with a given pattern. In particular, that's the way we'll decide whether a given extension methods is applicable or not with a given receiver expression. The notion of being 'subtype robust' is needed with extension methods, because we wouldn't want a matching process to fail at run time. That is, we must establish the guarantee that if the static type matches then the dynamic type will also match (and we rely on the general soundness property that the dynamic type of any given instance is always a subtype of any static type we could encounter for that instance).
Yes, that is true. But this is not a subtype robust pattern, so you would not be able to use it to specify applicable receiver types for some extension methods. If we allow type patterns in type tests then you could use it like this: void foo<U, V>(Map<U, V> map) {
if (map is Map<var X extends Y, var Y extends X>) {
// Known: `map is Map<invariant Z, invariant Z>` for some `Z`.
// We also know `Z == X == Y`, but the type system only knows that
// `X <: Y` and `Y <: X`, so we may need to use `X` and `Y` carefully,
// unless we get support for invariance such that we can say it directly:
if (map is Map<invariant X, invariant X>) {
// It's possible that the type system won't ever be smart enough to know
// that there is no need to generate code for the above test: It's guaranteed
// to be true, but it will probably be checked.
...
}
}
} |
For that, it's required that the type pattern is subtype robust. extension Foo<X,Y>
on Map<var X extends Y, var Y extends X>, X extends num, Y extends num { ... } This is a compile-time error because the given type pattern is not subtype robust: The problem is exactly what you say: We can check whether there is a match with the statically known type of any given receiver, but that doesn't provide a guarantee that a match on the dynamic type will succeed. Such a guarantee only exists with subtype robust patterns. But for usages where we do not insist that the match must be guaranteed to succeed at run time, there is no problem: bool foo(Map map) =>
map is Map<var X extends Y, var Y extends X>, X extends num, Y extends num);
main() {
foo(<num, num> {1: 1}); // True.
foo(<int, int> {1: 1}); // True, also if we cast it to `Map<num, int>`.
foo(<num, int>{}); // False.
} |
extension Foo<X,Y>
on Map<var X extends Y, var Y extends X>, X extends num, Y extends num {
void foo() {} // No need to use `X` or `Y`, any old method will do.
}
main() {
Map<num, num> map = <num, int>{};
map.foo(); // Static match succeeds, dynamic match fails.
} So the problem is simply that we have a "no such extension method" event (similar to
We could get this kind of knowledge if Dart is extended with invariance (#229, #214), but otherwise it would basically destroy the usefulness of such an extension (because Dart today has only few kinds of expressions whose type is exact, and it is not even specified). Because of this, I'm proposing that extensions just cannot have type patterns that aren't subtype robust. |
Yes, I do want to maintain a distinction here. When a method parameter is covariant there will be a dynamic type check, and that's an unavoidable consequence of having (1) covariance for generic classes, and (2) the ability to use a type variable of the enclosing class in a contravariant position in the signature of a class member. We will probably have some sort of invariance which will make it possible to eliminate this kind of dynamic check (gradually). The invocation of operator A member lookup is technically a different thing than a dynamic check on the type of an instance, and I think it makes sense to consider it different conceptually as well. We already have a clear and sound model for this area: A If we accepted |
Can do! (.. if we allow type patterns for promotion): main() {
var map = ...;
if (x is Map<var X extends Y, var Y extends X>, X extends num, Y extends num) {
map.foo();
}
} You can't really use a "testAndPromoteToNonNull" It might be useful to introduce an explicit matching test, which would specify a static extension by name and evaluate to true iff the given object matches the type pattern in the given extension: main() {
var map = ...;
if (x matches Foo) map.foo();
// Or maybe even..
(x as Foo).foo();
} But that's an extension that we can add on later, if it turns out to be sufficiently useful in practice.
I'm not convinced that it would be sufficiently useful. ;-) |
(Static overloading is the Oxycontin of programming. ;-) |
Scoped class extensions are intended to use type patterns and the associated matching to handle generics (that was one of the goals for having type patterns in the first place). I worked on the proposal for doing the same thing with scoped static extension methods first, because they are a bit simpler, but the idea would be exactly the same. extension ShortToString on Iterable<var X> {
// Similar to `toString`, but abbreviates the elements to an ellipsis.
String shortToString => "<$X>(...)";
}
on List<var X> {
String shortToString => "<$X>[...]";
}
on Set<var X> {
String shortToString => "<$X>{...}";
} |
I haven't worked my way through all the implications, so there might be some corner cases that we need to handle, but I expect arbitrary type patterns to be applicable to scoped class extensions (#177) as long as they are subtype robust. The example you mention should not create any problems: extension ShortToString on Iterable<var X extends num> {
// Similar to `toString`, but abbreviates the elements to an ellipsis.
String shortToString => "<$X>(...)";
}
on List<var X extends num> {
String shortToString => "<$X>[...]";
}
on Set<var X extends num> {
String shortToString => "<$X>{...}";
} As an example of something that would violate subtype robustness, you would not be allowed to introduce stronger constraints on subtypes: extension Whatever on Iterable<var X> {
bar() { ... }
}
on List<var X extends num> { // Compile-time error! See `main` to see why.
bar() { ... }
}
main() {
Iterable<Object> it = <String>[];
it.bar(); // Dynamic match failure: A `List<T>` only matches when `T <: num`!
} This is a plain subtype robustness violation, because the dynamic type |
What you don't get, then, is all the usual sanity checks on methods: If you have such a manually written chain of extension E on A {
foo() {} // Inherited
num bar(int i) => 2.0; // Overridden
}
on B /*subclass of A*/ {
int bar([num n]) { foo(); return 3; }
} If you are going to express a bunch of methods like this, some of them inherited and some of them overridden here and there in the target hierarchy, some of them changing signature in the override, how would you then be able to get the "proper" return type (if you call So the point is that you can use Of course, it would also be possible for a compiler to compile the invocation of |
The goal is not to avoid expressive power, the goal is to have a mode of thinking which will allow developers to understand Dart as a whole with a minimal amount of accidental complexity. For instance, if it will inform the developer about almost everything which is relevant to think "an extension method is a method" then that's a good way to avoid having a lot of unnecessary complexity built into the notion of an extension method in the first place. Then there may be a need to think about the fact that an extension can't have other things than instance methods (e.g., no state), and hence some programming idioms won't work. But that's a constraint, it is not an additional amount of complexity being piled on the semantics of an extension method as such. On top of that we have one new concept: At the call site, the actual type arguments of the receiver at the specified type pattern are extracted as specified in the type pattern of the extension (and that's a genuinely new mechanism). The ability of a method in an extension to use the type variables which are declared in enclosing scopes is again the same as usual: The type variables can be used just like other type variables, and they are known to have an actual value which satisfies the declared bound.
It wasn't a goal, so we shouldn't work ourselves to death to get it. The important point is that comprehension of the mechanism should not be hindered by accidental complexity.
It is not just "none of the above", because a third-party class could implement any of the classes that we do have a case for. The idea is that we will run the code for If you claim to "be a Of course, this is one thing that developers of extensions will have to have in mind, and it is something that third-party classes may not be built for. But this is a matter of culture: If we're working with extensions of an important type hierarchy, and third-parties provide new nodes to that type hierarchy (new subtypes) which are not just slight variants of the ones that we already have a case for (the code "ought to work for" a slight variant!), then it may be necessary to extend the extension. I showed one way to do that: Create a separate interface The culture part would then be to put pressure on those third parties to implement that interface. Alternatively, we could possibly integrate some of those third-party classes into the main subtype hierarchy, and add some cases to the extension to handle them. This is a more powerful mechanism than regular instance methods, because it will extend a subtype hierarchy with methods supporting late binding (aka OO dispatch), and they are automatically "inherited" by all subtypes. So if you're working on methods where the rationale is applicable ("you claim to be a I think the trick will be to use regular instance methods whenever that is possible, and then switch to extension methods (static or dynamic, depending on your needs) when needed, and at all times be aware of the rationale. For instance,
|
When a scoped class extension (#177) has a set of target types which are distinct classes (that is, stuff like However, the returned result is not a pointer to code, it is an extension object. The next step is to invoke the extension method on that extension object, and that's a completely normal method invocation. So it's a double-dispatching mechanism (the cost is about the same as two method invocations), which shouldn't be too surprising, given that we have a bunch of classes and a bunch of extensions, and we are calling the right extension method for that class.
Trying. ;-) var x = [1, 2, 3];
List<num> y = x;
var sum = y.sum(); I can't find any methods named If the cases have distinct classes (and the compiler generates "vtable" entries for all classes that it knows about at the location where the extension is declared, that is, including the ones that the platform can return) then we can expect to look up the correct extension object So we can certainly have some (worst) cases where the selection of the correct extension object is linear in the number of cases in the extension, and we can have other cases where it is O(1). It is possible that we can always optimize to some extent, but it will certainly never have to be worse than a linear search in the list of cases (which is what we would have with a chain of if statements).
If we have cases that target the same class and only differ in type arguments, using patterns that differ in subtle ways, then it is possible that we can't optimize anything: We will need to perform the matching operation on the more specific pattern, and continue with the next more general one if it fails. Each matching operation will bind type variables along the way, and if this is a violation of the declared bound then the match fails. |
Ah, I overlooked that, then we would have the following: extension E on Iterable<num> { // Extension object: eoE1.
sum() => e1;
}
on Iterable<int> { // Extension object: eoE2.
sum() => e2;
}
on Iterable<double> { // Extension object: eoE3.
sum() => e3;
} Again, unoptimized dispatch on y is Iterable<double>? eoE3.sum(y) : y is Iterable<int>? eoE2.sum(y) : eoE1.sum(y) where we would in general use something like a An optimization strategy which could be applied here is to note that we have a "lifted" set of distinct class types, so we could just extract the actual type argument of the receiver at |
@tatumizer wrote:
Exactly. (I say 'an extension' where you say 'an extension bundle', and I say 'a case of an extension' where you say 'an extension'; I'll stick to the terminology that I've used so far). This decision process is not trivial, but it is not very complex, either: A match between a type and a pattern produces a 'matched type', Basically, it's the pattern with the chosen value for each primitive type pattern inserted; so when
The description after that sentence is exactly correct. It is always correct to search backwards through all cases, and we have a guarantee that there will be a match, but for an exact match (like "the dynamic type is
Given that disambiguation is handled by forcing a linear specification (so the author of the extension must decide which is more specific when two types are not subtypes of each other), it is actually not necessary to consider a tree, a simple sequence will do. It might be possible to optimize various special cases using some search trees, but I don't have a final overview of that.
In the given example the static type We don't have any soundness issues, because there is no assumption (based on the initial case) which is not satisfied by the run-time choice. We could have a slightly more tricky situation like this one: extension Foo on Iterable<num> {
foo()=> e1;
} on List<num> {
foo() => e2;
} on Iterable<int> {
foo() => e3;
}
main() {
List<num> list = <int>[1, 2, 3];
list.foo();
} In this situation the statically matched case is Given that the dynamic type of the value o of an expression e will always be a subtype of the static type of e, we will never have the situation where the statically most specific matching extension case is later (nearer to the end) than the dynamically matching case, so the requirement "later case must be correct override of earlier case" is sufficient to ensure soundness of invocations. |
@tatumizer wrote:
Thanks!
Exactly, and this applies to all cases, even when we have cases which are unrelated classes (say,
That's a compile-time error, the first one must be a supertype of them all. But the whole set of cases in an extension must form a tree according to the subtype relation, so the situation where one case is the "root" of a tree of cases further down is present everywhere (noting that the tree may be just the root, because we have reached a leaf of that subtree). It might be a good idea to show syntactically that the first case is required to be a supertype of them all ... but since each case further down may have a similar subtree it doesn't seem like a very special property of the first case. It is already a very visible property to be at the front and have the name. ;-) |
Yes (search the proposal for
For static overloading? Yes. But it is definitely not true for extension methods: The main point of the whole 'scoped class extension' mechanism is that the extension object is selected dynamically, such that you invoke an extension method which is suitable for the dynamic type of the receiver, not just the one that you know about statically. The static properties are needed in order to ensure that this will always be possible. |
I think they might just stay separate statically: extension SumNum on Iterable<num> { ... }
on Iterable<int> { ... }
on Iterable<double> {
// All three cases would have this implementation, just changing the type,
// such that a compiler can generate optimal code for each type.
double sum() {
double result = 0.0;
for (var d in this) result += d;
return result;
}
}
extension SumBigInt on Iterable<BigInt> { ... }
main() {
var xs = [1.2, 3.4, 5.6];
double sum = xs.sum(); // No downcast, statically a `double`.
List<num> ys = xs;
double dsum = ys.sum(); // Downcast `num` to `double`, but runs the same code.
// `BigInt` stuff is similar, but with no subtype relation to `num`.
} |
@tatumizer wrote:
That's actually a property that we get for free with the proposal #309 where extension methods are generalized to be based on wrapper objects (which may then be compiled away as long as we stay within the subset which is similar to C# extension methods). |
Can someone provide an example for where this
The example above with using the static type information to change the implementation looks like it's more a matter of the Type variable being static, is that correct?
Or would this work dynamically as well? It just seems weird to me that we'd need the keyword |
First a bit of notation: Recent discussions about features in this area have used The fundamental notion of type patterns allows for a set of different mechanisms. One of the crucial distinctions is whether the semantics of a type pattern is based on the run-time values of type variables, or it is based on static information. In the case where the semantics is based on the run-time value the mechanism amounts to a kind of existential open: X id<X>(X x) => x;
const elements = [1, true, 'Hello', <int>{}, id, 2.5];
void f(Object o) {
if (o is List<final X>) {
for (var element in elements) {
if (element is X) o.add(element); // (1)
}
}
}
void main() {
f(<num>[]);
} At (1), an element of type This means that the loop will put exactly the objects into the list that are type correct elements of the list, and there will not be any run-time type errors. However, the semantics which is based on the statically known types can also be useful in some cases: Map<X, String> f(List<final X> list) {
X x = list.first;
return <X, String>{first: '$first'};
}
void main() {
Int8List xs = ...;
var map = f(xs); // `map` has type `Map<int, String>`.
} In this case we assume the static semantics. This implies that Note that it would not be safe to insert an element of type Another shortcoming with the static semantics is that there is no hope of getting a useful result from a type pattern matching operation where the existing static type does not embody the relevant information: void main() {
Object o = <int>[];
if (o is List<final X>) {
// `X == dynamic` or `X == Object?`, and we don't know anything new.
}
Iterable<num> iter = <int>[];
if (iter is List<final X>) {
// This should allow us to get `X == num`. But we might as well
// have used `iter is List<num>` in the first place.
}
} |
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.
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. Soint
matchesnum
, and all types matchdynamic
.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 thatT
matchesP
when it is possible to extract a value for each type variable introduced by a primitive type pattern inP
which satisfies the bounds, if any, and thenT
matches the result.Syntax
This proposal extends the Dart grammar with the following rules:
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
andDown
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 typeT
. The first step taken is to substituteDown
for every covariant occurrence ofNull
inT
, andUp
for every contravariant occurrence ofNull
inT
, yielding the termt
. In the following we will considerUp
andDown
as names of types. (SoIterable<Up>
is a superinterface ofList<Up>
, etc.) Subsequent steps are specified in the following rules:Let
P
be a type pattern of the formvar X
orvar X extends S
. In this caseX
corresponds toObject
with respect to (P
,Up
) (note that the bound cannot exist in this case becauseP
occurs in a contravariant position), andX
corresponds toNull
with respect to (P
,Down
). LetP
be a type pattern of the formvar X
orvar X extends S
, and lett
be a term different fromUp
andDown
. In this caseX
corresponds to[Null/Up, Null/Down]t
with respect to (P
,t
).Let
P
be a type pattern of the formC<P1, .., Pj, .., Pn>
wherePj
is a type pattern that introducesX
. In this caseX
corresponds toU
with respect to (P
,Up
) ifX
corresponds toU
with respect to (Pj
,Up
);X
corresponds toU
with respect to (P
,Down
) ifX
corresponds toU
with respect to (Pj
,Down
).Let
P
be a type pattern of the formC<P1, .., Pj, .., Pn>
wherePj
is a type pattern that introducesX
, and lett
be a term which has a direct or indirect superinterface of the formC<t1, .., tj, .. tn>
(note thatt
then cannot beNull
,Up
, orDown
). In this caseX
corresponds toU
with respect to (P
,t
) ifX
corresponds toU
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 introducingX
, 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 formP0 Function<...>(...)
whereP0
is a type pattern that introducesX
. In this caseX
corresponds toU
with respect to (P
,Up
) ifX
corresponds toU
with respect to (P0
,Up
), andX
corresponds toU
with respect to (P
,Down
) ifX
corresponds toU
with respect to (P0
,Down
). LetP
be a type pattern of the formP0 Function<...>(...)
whereP0
is a type pattern that introducesX
, and lett
be a term of the forms Function<...>(...)
. In this caseX
corresponds toU
with respect to (P
,t
) ifX
corresponds toU
with respect to (P0
,s
). In all cases, matching has failed if the corresponding type has a free occurrence of one ofY1..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 formP0 Function<Y1 extends B1, .. Ys extends Bs>(..., Pj, ...)
wherePj
is a type pattern that introducesX
. In this caseX
corresponds toU
with respect to (P
,Up
) ifX
corresponds toU
with respect to (Pj
,Down
), andX
corresponds toU
with respect to (P
,Down
) ifX
corresponds toU
with respect to (Pj
,Up
). In all cases, matching has failed if the corresponding type has a free occurrence of one ofY1..Ys
.Let
P
be a type pattern of the formP0 Function<Y1 extends B1, .. Ys extends Bs>(..., Pj, ...)
wherePj
is a type pattern that introducesX
, and lett
be a term of the formt0 Function<Y1 extends B1, .. Ys extends Bs>(..., tj, ...)
. In this caseX
corresponds toU
with respect to (P
,t
) ifX
corresponds toU
with respect to (Pj
,tj
). In all cases, matching has failed if the corresponding type has a free occurrence of one ofY1..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
andMap<var X, var Y>
are subtype robust. ButMap<var X, X>
is not, becauseX
occurs twice; this destroys subtype robustness because we could match statically withMap<num, num>
, bindingX
tonum
statically, and the run-time type could then beMap<int, num>
, which is a subtype ofMap<num, num>
but which fails to matchMap<var X, X>
.Similarly,
A<var X>, B<var Y extends X>
is not subtype robust becauseX
occurs twice; this destroys subtype robustness because the run-time type could implementA<int>
andB<num>
(which means that matching will fail), but the static type could still haveA<num>
andB<num>
as superinterfaces.Finally,
Function(var X extends num)
is not subtype robust becauseX
has a bound and occurs contravariantly. This breaks subtype robustness because the static type could beFunction(num)
and the run-time type could beFunction(Object)
, and this fails to match becauseX <: 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 ofx
is a subtype ofMap<exactly S, exactly T>
such thatT <: 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 containingNull
will have well-defined bindings for all type variables introduced by a type patterns clause. Otherwise, the patternList<List<var X>>
could soundly bindX
to any type when matched withList<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 anyT
, but the current specification ensures that the chosen binding ofX
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 ofX
toNull
), which means that whenever a typeS
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 typeList<Null>
whose static type isList<List<T>>
for someT
, and that method may create and return aList<List<Null>>
based on the binding ofX
, and the returned list will then actually have the typeList<List<T>>
, no matter whichT
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 matchMap<num, int>
and bindX
tonum
shows that this allows for a certain flexibility. But the patternMap<X, var X>
with the same type illustrates that it is possible to select a value forX
(namelynum
) such that the given typeMap<num, int>
is a subtype of the typeMap<num, num>
produced by the match. Still, the match fails because it only considers bindingX
toint
. This example illustrates why it makes sense to think about the occurrence ofX
which does not have the markervar
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 ofX
(as constraints onX
). 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.
The text was updated successfully, but these errors were encountered: