-
Notifications
You must be signed in to change notification settings - Fork 211
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
[Static extensions] How does inference work for constructors defined in static extensions #4053
Comments
I like it. This inference behavior is clear and non-surprising. I'm interested in the case of multiple extensions on extension E2<S, T> on Map<bool, T> {
factory Map.bar(S x, T y) => {false : y};
} How do we know which |
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. 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 In both cases void main() {
var list = NumList.singleton(4);
List<int> list2 = NumList.banana();
} that the A There are two possible ways to do make that fail:
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 Any two static extensions with the same name on the same base class will conflict. Another approach is then to try to vet the instantiated receiver type against the 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 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. Or something. |
I don't know what the
I don't know what this means, can you elaborate?
I believe that my proposal will infer |
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. |
I do. Fixed. |
@leafpetersen wrote:
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 So let's say that the only possible resolution of the instance creation is the constructor
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 This matches very well with the use of inference based on
The proposal uses an even more verbose form to describe the explicitly resolved invocation of For (The reason is that I do not think it's acceptable to use an approach to inference whereby the static type of I'd want this step to yield a constraint that But if we assume that we have #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 var x2 = E_Map_bar<_, num>("hello", 3); Normal inference will now succeed and find that the first type argument should be For Map<Object, Object> x3 = E_Map_bar("hello", 3); // Result `E_Map_bar<String, Object>`. Finally, with 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 // 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 What I'm proposing is that we have the step where
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 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 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. 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. 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 If we try to solve it as above, the downards inference constraints are 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 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 |
I'm coming at this a little cold and forgive me if this is too tangential:
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 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.
I would disallow generative extension constructors entirely. I didn't even know we were considering those. |
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, When you write So the error here would be either:
if we just ignore inapplicable constructors entirely, or some text describing that no instantiation of 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. We are still in the position where Map<String, bool> foo = Map.bar('a', true); needs to resolve It then checks which static extensions are available that has a static member or constructor with base name Checking for type-based applicability means looking at the receiver type. Here Since the extension was not applicable to Is this confusing? Yes! Maybe we should only allow static constructors if the type parameters are the same as If you do the The next obvious questions could be:
|
Here's how the proposals here would respond.
The idea is that an invocation of a constructor like 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 For a class like So we can subset the applicable type arguments with a constructor in a static extension by writing an If you want to specify the type arguments of the static extension directly then you can use a different syntax: With 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 We can use the ability to specify the type arguments of the constructor indirectly to decompose the actual type arguments (assuming 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 |
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
The text was updated successfully, but these errors were encountered: