Open leafpetersen opened 2 months ago
I like it. This inference behavior is clear and non-surprising.
I'm interested in the case of multiple extensions on Map
with slightly different type arguments in the on-clause of the extensions. For example, we may have the following declaration, in addition to E
:
extension E2<S, T> on Map<bool, T> {
factory Map.bar(S x, T y) => {false : y};
}
How do we know which bar
should be applied and how should we construct the replacement context in that case?
I wrote in a different issue that we probably want to treat "raw" constructor invocations specially, carrying the raw-ness through the applicability check, and then infer type arguments on the way back. Just being able to do:
static extension NumList<T extends num> on List<T> {
List.singleton(T value) : this.filled(1, value);
factory List.banana() { print("Banana"); return this.empty(); }
}
void main() {
var list = List.singleton(4);
List<int> list2 = List.banana();
}
and having it inferring List<int>
in both cases is such a fundamental way of using constructors that it will be incredibly confusing if it doesn't work like that.
In both cases List
is a raw type clause (an identifier which denotes a generic declaration).
What we want is that the invocations work exactly like they would on the extension:
void main() {
var list = NumList.singleton(4);
List<int> list2 = NumList.banana();
}
that the List.xyz
term denotes the constructor NumList.xyz
, and type inference continues from there. (Where we have type inference defined for direct invocations of the members too.)
If they apply. That's the catch, we have to figure out first whether they apply, which may depend on inferred type arguments, depending on how we define applicability.
A List<String>.singleton("a")
shouldn't be allowed to run the static extension constructor. That requires binding the type parameter <T extends num>
to String
and then executing code with that binding. That's a no-go.
There are two possible ways to do make that fail:
List<String>.singleton("a")
at all.The latter allows another static extension member with the same name to apply instead.
The easy way out is to say that a static extension is applicable if the static namespace of the extension's uninstantiated on
type is the same as the static namespace of the target type clause.
(Maybe only constructors are applicable to T<...>.m
references, and only non-constructors to T.m=
or T.m<...>
references.)
Any two static extensions with the same name on the same base class will conflict. That's always going to be the case for static members, but not necessarily for constructors which can differ on instantiation of a generic receiver type.
Another approach is then to try to vet the instantiated receiver type against the on
type of the extension.
That requires an instantiated receiver type.
A call of List<int>.singleton(2)
is clearly instantiated, we can check that NumList<T>
's on
type List<T>
can be solved against List<int>
to an instantiation T = int
. Then we check if the instantiated on
type List<int>
is a good match for the instantiated receiver type List<int>
(it is!), and the extension is applicable.
(And an on
type of List<num>
would not be applicable for a constructor with receiver type List<int>
.)
We would want a raw receiver type with a context type to do downwards inference then, so:
List<String> l = List.singleton("a");
would infer List<String>.singleton(...)
for the NumList
constructor, from the context, and then consider that a non-raw
receiver type that the extension does not apply to.
So yes, downwards inference where possible, then applicability check on the resulting non-raw type. But also preserving a raw type with no context type so it can be inferred from the arguments.
For the
extension E2<S, T> on Map<bool, T> {
factory Map.bar(S x, T y) => {false : y};
}
Map<bool, String> m = Map.bar(42, "a");
example a context type can only help infer the value type.
We have a potential constructor on Map
, so we try to do downwards inference on the Map
as a constructor, and we can infer T = String
, but nothing for S
. No contradiction, so the extension applies.
Then, if it's the only one that applies, continue inferring types for E2<S, T>.bar(42, "a")
with T=String
, which may solve for S=int
.
Or something.
@lrhn
static extension NumList<T> on List<T extends num> {
I don't know what the T extends num
is doing above, do you mean to have that in the binding?
I wrote in a different issue that we probably want to treat "raw" constructor invocations specially, carrying the raw-ness through the applicability check, and then infer type arguments on the way back.
I don't know what this means, can you elaborate?
and having it inferring
List<int>
in both cases is such a fundamental way of using constructors that it will be incredibly confusing if it doesn't work like that.
I believe that my proposal will infer List<int>
for both of your examples.
@chloestefantsova
How do we know which
bar
should be applied and how should we construct the replacement context in that case?
My take is that adding constructor overloading via extensions is probably an anti-goal. My initial starting pitch would be that:
If we really want to do overloading... I don't know. That should probably be a separate issue/discussion.
do you mean to have that in the binding?
I do. Fixed.
@leafpetersen wrote:
how do we expect type inference to work for constructors defined in static extensions
In an earlier version of the proposal, in this section, I proposed the use of a generic function to decide whether or not any given extension would be applicable for a given instance creation expression. Your idea shares some elements with that approach. For the current version, in this PR, I used a declarative approach, just specifying the requirements that must be satisfied by the type inference step.
I'll try to explore the relationship between that approach and the one described in this issue. Here is the example extension:
extension E<S, T> on Map<int, T> {
factory Map.bar(S x, T y) => {3 : y};
}
The rules I've proposed take a number of steps in order to decide that an expression like Map.bar("hello", 3)
must be an invocation of the constructor declared by E
. In particular, we check that bar
isn't a static member declared by Map
and that there are no accessible extensions with on-class Map
that declare such a static member. It's an error if we find both a static member and a constructor.
So let's say that the only possible resolution of the instance creation is the constructor E.Map.bar
.
How should inference work on calls to this constructor? That is, given
test() {
var x1 = Map.bar("hello", 3);
var x2 = Map<int, num>.bar("hello", 3);
Map<Object, Object> x3 = Map.bar("hello, 3);
Map<Object, Object> x4 = Map<int, num>.bar("hello, 3);
}
We now decided that Map.bar("hello", 3)
is treated as E.Map.bar("hello", 3)
. E
accepts two type arguments and they will determine the type of the expression as a whole as well as the parameter types. The type parameters of E
must have suitable bounds such that every choice of actual type arguments to E
that satisfies the bounds will also yield an instantiated constructor return type that satisfies the bounds. So we can rely on inference selecting type arguments S0, T0
to E
, and then just insert them into the constructor return type Map<int, T>
, yielding Map<int, T0>
, which is then the type of the instance creation as a whole. (In the example, the type parameters of E
do not actually have any bounds.)
This matches very well with the use of inference based on E_Map_bar
:
how do we produce the type arguments to turn each implicit invocation into an explicit invocation of the form E<T1, T2>.Map.bar("hello", 3); for some T1 and T2?
My initial thinking is that we can draw intuition by considering the original definition as essentially defining a generic static method equivalent to the following:
Map<int, T> E_Map_bar<S, T>(S x, T y) => E<S, T>.Map.bar(x, y);
The proposal uses an even more verbose form to describe the explicitly resolved invocation of E.Map.bar
, namely E<S, T>.Map<int, T>.bar(x, y)
. This is simply a notation that shows all the types explicitly, which ensures that it is trivial to check consistency (substitute the actual type arguments of E<S, T>
into the constructor return type, that must yield the type Map<int, T>
, which is then also the type of the expression as a whole).
For Map<int, num>.bar("hello", 3)
, which is treated as E.Map<int, num>.bar("hello", 3)
, we need to infer type arguments to E
such that the instantiated constructor return type is Map<int, num>
. This is the point where I'm proposing that we require Map<int, num>
, not just a subtype or a supertype.
(The reason is that I do not think it's acceptable to use an approach to inference whereby the static type of Map<int, num>.bar("hello", 3)
ends up being Map<int, Object>
or any other proper supertype, and also not Map<int, int>
or any other proper subtype.)
I'd want this step to yield a constraint that T
must be equal to num
, not just a subtype that by sheer luck ends up being num
, or a supertype likewise. I wouldn't expect to be able to get this effect by specifying a function like E_Map_bar
above and perform inference on an invocation of that function.
But if we assume that we have https://github.com/dart-lang/language/issues/3963 then we could express it as follows: First use type equality constraints to find the exact value of zero or more type arguments passed to E
. This yields the constraint that the 2nd type argument (passed to T
) must be num
. Next, infer:
var x2 = E_Map_bar<_, num>("hello", 3);
Normal inference will now succeed and find that the first type argument should be String
, and this allows us to transform E.Map<int, num>.bar("hello", 3)
to E<String, num>.Map<int, num>.bar("hello", 3)
.
For Map<Object, Object> x3 = Map.bar("hello", 3)
we resolve Map.bar
as E.Map.bar
as usual, and infer:
Map<Object, Object> x3 = E_Map_bar("hello", 3); // Result `E_Map_bar<String, Object>`.
Finally, with Map<Object, Object> x4 = Map<int, num>.bar("hello, 3);
, we resolve as usual and infer by equality constraints that the 2nd type argument to E
must be num
and then infer
Map<Object, Object> x4 = E_Map_bar<_, num>("hello", 3); // Result `E_Map_bar<String, num>`.
Here is an example where it makes a difference whether we insist on equality constraints to connect E
and Map
or we are just using standard inference on an invocation of F_Map_bar
:
// Let's say this is the only accessible extension.
extension F<S extends num, T> on Map<S, T> {
factory Map.bar(S x, T y) => {x : y};
}
// Used to emulate inference.
Map<S, T> F_Map_bar<S extends num, T>(S x, T y) => F<S, T>.Map<S, T>.bar(x, y);
void main() {
Map<num, Object> x1 = Map<double, String>.bar(1.5, 'Hello!');
// Corresponding inference based on F_Map_bar:
Map<num, Object> x2 = F_Map_Bar(1.5, 'Hello!'); // Result `F_Map_bar<num, Object>`
}
If we rely on standard inference of the invocation of F_Map_bar
in the given context then we will (by downward inference) choose the actual type arguments num
and Object
. I can't easily see a way to take the type arguments which are passed explicitly to Map
into account, and we end up creating a Map<num, Object>
. I think this is not acceptable when the syntax says Map<double, String>
.
What I'm proposing is that we have the step where Map<double, String>.bar(1.5, 'Hello!')
is resolved as F.Map<double, String>.bar(1.5, 'Hello!')
, equality inference is then used to make it F<double, String>.Map<double, String>.bar(1.5, 'Hello!')
, and that is then the final result. We will then create a Map<double, String>
as requested by the developer, and it is checked that Map<double, String>
is assignable to Map<num, Object>
as usual.
Note that I'm not proposing we specify this via the desugaring to a static method E_Map_bar as written above: this just provides the underlying semantic model which motivates my sketch of how inference should work above.
Certainly.
A couple of things to keep in mind, regarding the expressive power that we may include or exclude based on our choices about how to perform type inference:
Constructors in static extensions can emulate generic constructors in some cases. Assume that we want the following:
class A {
final int i;
A(this.i);
A.computed<X>(X x, int Function(X) fun): this(fun(x)); // A generic constructor.
}
void main() {
A a = A(42);
a = A.computed('Hello!', (s) => s.length);
}
Here's the emulation, using static extensions:
class A {
final int i;
A(this.i);
}
static extension AExtension<X> on A {
A.computed(X x, int Function(X) fun): this(fun(x));
}
// `main` is unchanged.
We can't provide an actual type argument as suggested in the generic constructor issue (that would be A.computed<String>(...)
), we'll have to use AExtension<String>.A.computed(...)
, but it does look the same as long as all type arguments can be inferred.
Another difference is that a a static extension can only add a generative constructor to a class if it is redirecting, so if we want a generative non-redirecting generic constructor then it can't be expressed as a declaration in a static extension. Still, this approach does allow us to emulate a generic constructor in a lot of situations.
Constructors in static extensions can also emulate conditional constructors. Assume that we want the following:
// Let's just pretend that `List` has this extra, conditional constructor.
class List<E> {
...
if <E extends Comparable<E>>
List.sorted(Iterable<E> iter) {...}
}
class A {
final int i;
A(this.i);
}
void main() {
var xs = List.sorted(['b', 'a']); // OK.
var ys = List.sorted([A('b'), A('a')]); // Compile-time error.
}
Emulation using extensions:
class List<E> ... // Doesn't declare the constructor `List.sorted`.
static extension ListSortedConstructor<E extends Comparable<E>> on List<E> {
List.sorted(Iterable<E> iter): this.of(iter.sort((a, b) => a.compareTo(b)));
}
// Class `A` and `main` are unchanged.
I think the inference rules here do work. The static "constructor function" is likely the function you would get by doing a List.bar
tear-off, similarly to how we get functions with other type parameters when tearing constructors off through a type alias. Fx:
typedef MM<K1, K2, V> = Map<K1, Map<K2, V>>;
var mmf = MM.filled; // Static type `Map<K1, Map<K2, V>> Function<K1, K2, V>(int, Map<K2, V>)
The examples do not have extra bounds, which is where I have some problems. (It may assume that a constructor is applicable if it has the correct name and base type, independent of type parameters. Or it may just not hit any case where it matters.)
Factory constructors are usually easier, because they can be used covariantly - they can return a subtype of the constructed type. Generative constructors must be able to initialize an object of exactly the constructed type. (Or we can disallow using a generative extension constructor as a super-constructor in a non-redirecting generative constructor, and avoid that complication.)
Consider:
class O<T1, T2 extends num> {
O.rn(T v1, List<T2> v2);
}
extension E<S1, S2 extends int> on O<S1, S2> {
O.ex(S1 v1, String v2) : this.rn(v1, <S2>[if (0 is S2) v2.length as S2]);
factory O.re(S1 v1) = P<S1, S2>.re;
}
class P<R1, R2 extends int> extends O<R, R2> {
P.re(S1 v1) : super.ex(v1, "$v1");
}
If I do O<String, num>.ex("a", "b")
then it must be a compile-time error.
The type parameter S2
has bound int
, so no code can be allowed to run with it bound to num
.
If we try to solve it as above, the downards inference constraints are S1 == String
and S2 == num
. Since S2 == num
is not valid for S2 extends int
, the invocation is invalid. If we can't solve validly, it's an error. That's reasonable.
We also cannot allow a class like:
class Q<R1, R2 extends num> extends O<R, R2> {
Q.re(S1 v1) : super.ex(v1, "$v1");
}
Here the only reference to the extension E
is the super.ex
invocation.
That invocation needs to do the same type inference with target type O<R, R2>
, and R2 extends num
is not a valid value for S2 extends int
.
The super-constructor invocation is invalid, because super.ex
construtor that otherwise behaves as if it was on O
is not valid for all instantiations of O
, like any constructor actually on O
must be.
(Ok, let's not allow using static extension constructors as super-constructors. They're only for creating instances.)
Should we just not use the type parameters of the extension for constructors. What if we did:
extension E<S1, S2 extends int> on O<S1, S2> {
O<X1, X2 extends num>.ex(X1 v1, String v2) : this.rn(...);
}
instead, requiring that any extension constructor on O
must provide its own type parameters that accept all valid type arguments to O
.?
It's extra verbose when the type parameters match up. We could allow you to use the type parameter of the extension if they match the type parameters of the on-class, and if not, all extension constructors have to add their own type parameters that do match.
(Or we could allow all constructors to have more restricted type parameters than the class itself.)
I'm coming at this a little cold and forgive me if this is too tangential:
extension E<S, T> on Map<int, T> { factory Map.bar(S x, T y) => {3 : y}; }
Given that, if I were to do:
Map<String, bool>.bar('s', true);
Then I am calling a constructor declared in an extension whose on type is Map<int, T>
. But the constructor is returning an object which is not a Map<int, T>
for any T
. Doesn't that seem... weird? You've got a constructor that doesn't return the type of its surrounding thing (the extension and its on type here), which violates the assumption of constructors. And you've got an extension that's a constructor but that returns an object wouldn't match the on
type of the thing you just called. It almost seems like the constructor shouldn't be applicable, or this should be some kind of error.
I understand that static extensions make this weird because the extension is resolved on something more like the on declaration than the on type. But something feels very fishy here to me.
Generative constructors must be able to initialize an object of exactly the constructed type. (Or we can disallow using a generative extension constructor as a super-constructor in a non-redirecting generative constructor, and avoid that complication.)
I would disallow generative extension constructors entirely. I didn't even know we were considering those.
extension E<S, T> on Map<int, T> { factory Map.bar(S x, T y) => {3 : y}; }
Given that, if I were to do:
Map<String, bool>.bar('s', true);
Can you do that at all? That's part of the question here. (And the answer is probably "no".)
If you write the explicit static extension member invocation, E<String, bool>.bar('s', true)
, or as the proposal syntax says it, E<String, bool>.Map.bar('s', true)
, then you are effectively calling a constructor on Map<int, bool>
, not on Map<String, bool>
. That's the Map
type that E<String, bool>
refers to. (Just as if E
has been typedef E<S, T> = Map<int, T>;
and bar
had been declared on Map
, somehow, while still referring to S
.)
When you write Map<String, bool>.bar('s', true)
, we need to find an available and applicable static extension base-named bar
on the type Map<String, bool>
, and then E.Map.bar
does not apply, because its on
type is Map<int, T>
and there is no solve for S
and T
that will make Map<int, T>
be Map<String, bool>
. (Or even just "a subtype of", since this is a factory constructor, but it's not a given that we won't require the instantiated static extension on
type to be equal to the constructor target type, at least up to mutual subtyping. It's definitely needed if we allow generative constructors.)
So the error here would be either:
Map<int, T>
does not have a constructor namedbar
if we just ignore inapplicable constructors entirely, or some text describing that no instantiation of E<S, T>
can be found that makes Map<int, T>
be Map<String, bool>
as requested.
If we disallow generative extension constructors entirely, that means disallowing redirecting generative constructors, which we could support. Non-redirecting ones were never going to be possible. That reduces the problem, but does not remove it.
We are still in the position where
Map<String, bool> foo = Map.bar('a', true);
needs to resolve bar
at a point where it doesn't if it's a static member access or a constructor access.
It then checks which static extensions are available that has a static member or constructor with base name bar
.
It findsE
.
Here we can say that if it finds more than one, there is a conflict, and we give up. Or we can take one step further and check if extension is applicable, which only makes sense for constructors. Static members are always applicable, and have no access to type parameters.
Checking for type-based applicability means looking at the receiver type. Here Map
is a raw constructor receiver for a potential constructor invocation, so do type inference based on the context type to make it Map<String, bool>.bar(...)
.
Then check if E<S, T> on Map<int, T>
is applicable to Map<String, bool>
. It isn't, no possible way.
(If there was any possible solution for S
and T
where the on
-type was related to Map<String, bool>
, we could choose to solve for either ==
or <:
relation between the instantiated on
type and the specified type it applies to. Even for factory constructors, I'd go with ==
.)
Since the extension was not applicable to Map<String, bool>
, we can then remove E
from consideration again and let another static extension member, or just say that there is an error.
Is this confusing? Yes!
Maybe we should only allow static constructors if the type parameters are the same as on
type's.
If the static extension type parameters are not the same as those of the generic class (up to alpha-equivalence), or are not forwarded directly (no swapping positions), so anything but extension Name<A, B> on Map<A, B>
, then you can't declare an extension constructor at all. Or allow swapping positions, and just require the type parameters in the in
type are used lineraly and has the same bound as the corresponding parameter in the on
type class.
(So extension E<A, B extends int, C> on Map<C, A>...
is OK. Solving backwards is still trivial and always possible.)
If you do the on Map<int, T>
thing, it's not declaring any constructors on Map<int, T>
. You can still declare static members on Map
and ignore the type parameters, but to declare constructors on Map<A, B>
, we must be able to see that we can solve for A
and B
for any instantiation of Map
. It must match all Map
types to be allowed a constructor.
The next obvious questions could be:
on
type, so Map<S, T>
directly means E<S, T>
. No. Because we're lucky that E
has two type parameters here. For extension F<X extends String> on Map<X, X> { factory Map.baz(X value) => {value: value}; }
, writing Map<int, int>.baz(1);
is meaningless. The F.Map.baz
constructor refers to X
which has a bound, we can't just ignore that. We have to solve for the type parameters of the extension somehow, and the on
type is the only thing we have that constrains them.factory Map<X, Y>.baz(...) {...}
. Then the constructor is just a static extension on the static namespace of the on
type, it's not on the instantiated on
type itself. That constructor cannot reference the type parameters of the extension at all. (Then we can allow that syntax in classes too, so you can do factory List<T extends Comparable<T>>.sorted(Iterable<T> values) => ...
. #1899. I want this.)extension E<S, T> on Map<int, T> { factory Map.bar(S x, T y) => {3 : y}; } ```dart
Given that, if I were to do:
Map<String, bool>.bar('s', true);
Then I am calling a constructor declared in an extension whose on type is
Map<int, T>
. But the constructor is returning an object which is not aMap<int, T>
for anyT
Here's how the proposals here would respond.
Map<String, bool>.bar('s', true)
is a compile-time error. In order to not be a compile-time error, it must be possible to infer two actual type arguments, bind them to S
and T
, respectively, and thus yield the instantiated on
type Map<String, bool>
. But there's no way we can bind S
and T
to anything such that Map<int, T>
becomes Map<String, bool>
. Hence, type inference must fail.
The idea is that an invocation of a constructor like Map<T1, T2>.bar(arg1, arg2)
must have static type Map<T1, T2>
, such that a reader can understand what we're creating without knowing whether the constructor is declared by Map
itself, or it is added to Map
by a static extension.
A constructor which is added by a static extension has a small amount of extra expressive power: it doesn't have to repeat the declaration of the type parameters of the on
class, it can declare any number of actual type arguments and use them in whatever way it wants in the on
type, as long as every possible binding of the type variables will yield an on
type whose type arguments satisfy the bounds on the on
class.
For a class like Map
whose type parameters do not have a bound, we can use any type arguments we want in the on
type. With E
we're using Map<int, T>
, which illustrates that we can use ground types as well as type variables in any combination we might desire.
So we can subset the applicable type arguments with a constructor in a static extension by writing an on
type and some type parameters that can't express all the type argument lists that the on
class supports (in the example: we insist that Map.bar
can only create maps whose key type is int
). Moreover, we can decompose the type arguments, as shown below.
If you want to specify the type arguments of the static extension directly then you can use a different syntax: E<String, bool>.Map.bar(e1, e2)
. This has exactly the same meaning as E<String, bool>.Map<int, bool>.bar(e1, e2)
, which also implies that e1
must have a static type which is assignable to String
, and e2
must be assignable to bool
.
With E<String, bool>.Map.bar('s', true)
we can't see the type of the result, but it is fully determined because E<String, bool>
binds S
to String
and T
to bool
, which implies that the instantiated on
type is Map<int, bool>
. I think it's crucial (for code comprehensibility) that Map<int, bool>.bar(...)
has static type Map<int, bool>
and not a subtype or supertype thereof. The idea is that we must preserve the property that the type of a constructor invocation is "obvious".
When it comes to inference, the context type can play a role as usual:
Map<int, T> mapBar<S, T>(S s, T t) => E<S, T>.Map.bar(s, t);
void main() {
Map<Object, Object> map = Map.bar('s', true);
// .. is treated like:
Map<Object, Object> map1 = mapBar('s', true); // Inferred as `mapBar<String, Object>...`.
}
The new map has static (and dynamic) type Map<int, Object>
.
We can use the ability to specify the type arguments of the constructor indirectly to decompose the actual type arguments (assuming --enable-experiment=inference-using-bounds
):
static extension E<X extends Iterable<Y>, Y> on Map<Y, X> {
factory Map.baz(X x) => {x.first: x};
}
// Just used to illustrate inference.
Map<Y, X> mapBaz<X extends Iterable<Y>, Y>(X x) => {x.first: x};
void main() {
var map = Map.baz([1]); // Inferred as `mapBaz<List<int>, int>(<int>[1])`.
print(map.runtimeType); // '_Map<int, List<int>>'.
}
This is again something that we can't do using a constructor which is declared in Map
.
Bubbling up this discussion from comments in #3835 , how do we expect type inference to work for constructors defined in static extensions. Ignoring the question of whether we re-use the existing
extension
syntax or use a newstatic extension
syntax, we have the general problem of going from a constructor call of the formC.name(args)
orC<T1, ...., Tn>.name(args)
which is defined on an extensionE
which has type argumentsX1.... Xk
and on typeT_on
to a specific choice of type arguments forX1....Xk
. As a concrete example to work from, consider:How should inference work on calls to this constructor? That is, given
how do we produce the type arguments to turn each implicit invocation into an explicit invocation of the form
E<T1, T2>.Map.bar("hello", 3);
for someT1
andT2
?My initial thinking is that we can draw intuition by considering the original definition as essentially defining a generic static method equivalent to the following:
This gives an obvious intuition as to how to infer calls to
Map.bar
without explicit type arguments: we view an invocation ofMap.bar(args)
in downwards contextK
as essentially equivalent to an invocation ofE_Map_bar(args)
in downwards contextK
. For the above examples with no explicit type arguments, this then results in the following inferred explicit invocations:Note that in the case of
x3
, downwards inference pins the type argumentT
toObject
when the subtype matchMap<int, T> <# Map<Object, Object>
is performed, but leavesS
unconstrained.S
is then filled in via upwards inference based on the first argument to the constructor.How then should we treat the cases with explicit type arguments (
x2
andx4
)? My initial intuition is that we get the desired result by treating the explicit instantiation exactly as above, except replacing the downwards contextK
by the explicitly instantiated type on which the constructor is called. Note that in the explicit instantiation case,K
adds nothing to the inference process. So here, we would treatvar x2 = Map<int, num>.bar("hello", 3);
the same as if we had the invocationE_Map_bar("hello", 3)
in downwards contextMap<int, num>
, and likewise forMap<Object, Object> x4 = Map<int, num>.bar("hello, 3);
. The result would be the following inferred explicit invocations:In each case, the downwards context (if any) is replaced by
Map<int, num>
which in turn fixesT
to benum
via the subtype match ofMap<int, T> <# Map<int, num>
solving forT
andS
is inferred via upwards inference from the first argument to the constructor.The intuition for replacing the downwards context is essentially that by writing
Map<int, num>.bar(...)
the user has expressed the requirement thatbar
produces aMap<int, num>
.Note that I'm not proposing we specify this via the desugaring to a static method
E_Map_bar
as written above: this just provides the underlying semantic model which motivates my sketch of how inference should work above. The specification itself is essentially just a use of the already existing generic type argument inference process, with a particular choice of downwards context, type arguments, arguments, and type parameters to solve for.@eernstg @stereotype441 @chloestefantsova @johnniwinther WDYT ? Does this make any sense? Do you see problems with my rough sketch above? Alternative proposals?
cc @dart-lang/language-team