-
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
Constructor tear-offs: how to treat tear offs from generic classes #1586
Comments
I'm positive towards this approach. I think it would be completely uncontroversial if it wasn't for the prospect of generic constructors. I'm slightly worried about users conflating class generics and method generics and being confused why If we treat constructor tear-off as equivalent to tearing off a static helper function, like: static List<E> List$filled$tearoff<E>(int count, E fill) => List<E>.filled(count, fill); then we get instantiated tear-off for free from what we do for normal function tear-off. (And it defines when we do canonicaization). I'd still also want to allow explicit instantiation, List<int> Function(int, int) f = List.filled; There is no shorthand for the function type, even when all you want is the return type. If you write anything in the variable type, you must write it all because you turn off type inference for the entire variable type. The choice of how to instantiate is driven by context type. That's something we decided not to use to distinguish a type literal from an unnamed constructor tear-off — but not only because it uses the context type, also because it's unwieldy and error-prone compared to callable object tear-offs. The only real issue is what happens if we add generic constructors. Which won't matter until we do that, so as long as we believe it can be solved, we don't necessarily need to worry about the complexity of the solution yet. Uncurrying the type parameters is also the only solution I could find that made sense. It might be breaking if we introduce type arguments to existing constructors, because then For a generic class and constructor, it's less clear that we can just defer it to a normal function tear-off of a static helper function, because we need to do a partial instantiation. With: static A<T> named$tearoff<T, S>(S x) => A<T>.named<S>(x); we don't have an existing definition of how to specialize only For: class A<T> {
A.named<S>(S x);
} would it be possible to instantiate That is, are all of the following valid: var x = A.named; // A<T> Function<T, S>(S)
A<T> Function<T, S>(S) y = A.named;
A<int> Function<S>(S) z = A.named;
A<T> Function<T>(int) w = A.named; // <-- specialize only `S`.
A<int> Function(String) v = A.named; We will have an issue deciding which type parameter is being specialized if we allow both. If we only allow the class parameter to be specialized by itself, then it's clear from the number of type arguments in the result type what happens. Might be safer that way, but it sets a precedent for partial type instantiation to take the first parameter (if that is ever something we'd want to do in general for normal generic functions). There are tricky cases where class B<T> {
B.named<S extends T>(S x);
} where B<T> Function<T>(int x) p = B.named; would not be valid, but that's a type issue because we can't instantiate Alternatively, we do not provide partial instantiation. The constructor tear-off of |
In order to get the enhanced expressive power, I think it would be useful to let a constructor tearoff be as generic as the class and the constructor admits. Generic function instantiation could then be applied implicitly to the torn-off function, using the same rules as the ones that we have for generic functions/methods already. That amounts to the same thing as @leafpetersen's proposal at
However, I think we should take the opportunity to enable explicit instantiation of tearoffs (for functions, methods, constructors). It is an inconvenient anomaly that we can obtain the instantiation via a context type, but we can't express it directly. A possible syntax for this could use class C<X> {
C(int i);
C.named(String s);
C.generic<Y>(Y y, X Function(Y) f); // If we get those.
void m<Z, W>(Z z, W w) {}
}
List<X> f<X, Y>(X x, Y y) => [x];
void main() {
f; // Type `List<X> Function<X, Y>(X, Y)`, as today.
f<int>.call; // Type `List<int> Function<Y>(int, Y)`.
f<int, String>.call; // Type `List<int> Function(int, String)`.
var c = C<int>();
c.m; // Type `void Function<Z, W>(Z, W)`, as today.
c.m<bool>.call; // Type `void Function<W>(bool, W)`.
c.m<bool, bool>.call; // Type `void Function(bool, bool)`.
C.new; // Type `C<X> Function<X>(int)`.
C.named; // Type `C<X> Function<X>(String)`.
C<String>.new; // Type `C<String> Function(int)`.
C<double>.named; // Type `C<double> Function(String)`.
C.generic; // Type `C<X> Function<X, Y>(Y, X Function(Y))`.
C<int>.generic; // Type `C<int> Function<Y>(Y, int Function(Y))`.
C.generic<bool>.call; // Type `C<X> Function<X>(bool, X Function(bool))`.
C<int>.generic<bool>.call: // Type `C<int> Function(bool, int Function(bool))`.
} If we consider the support for currying (by passing fewer type arguments than the declared formals) too error prone then we can always make it an error instead. The point is that So if we start using I think it's a nice property that we may think of the tearoff We would use the same rule as today where an ordinary method invocation (like void main() {
f(1, true); // Normal function call, inferring `<int, bool>`.
f<int>.call(1, true); // Would tear off a function object, doing `(f<int>.call)<bool>(1, true)`.
f<int, bool>.call(1, true); // Would tear off, but could also be desugared as `f.call<int, bool>(1, true)`.
C.new(2); // Allowed in the proposal already, inferred as `C<dynamic>(2)`.
C.named('bar'); // Inferred as `C<dynamic>.named('bar')`.
C<String>.new(2); // Again in the proposal, `C<String>(2)`.
C<double>.named('bar'); // Possible today.
C.generic(3, (i) => i.isEven); // Ideally, inferred as `C<bool>.generic<int>(...)`.
C<int>.generic(true, (b) => 3); // Ideally, inferred as `C<int>.generic<bool>(...)`.
C.generic<bool>.call(true, (b) => 3); // Ideally, inferred as `C<int>.generic<bool>(...)`.
C<int>.generic<bool>.call(true, (b) => 3): // No inference.
} We have an anomaly in that the invocations that are possible today must make a choice for any omitted type arguments (so we get an instance of Other than that, the interactions between the torn off function objects and the forms where the function is invoked directly seem to be rather natural. |
If we want to enable explicit instantiation of function tear-offs, then we can also consider just allowing |
The reason why I tried to come up with something other than I think forms like But if #123 will just work without any difficulties then we could certainly use the same syntax for tearoffs. |
My preference is that a tearoff of a constructor on a generic class results in a generic function (other than where inference fills it in) as in Leaf's alternative. This feels the least surprising to me, and it buys some power than I think is unlikely to be confusing for users, even if it doesn't see much use. If it turns out to be a large burden to implementations to tear off as a generic function that could convince me otherwise. I also do think it would be value to implement #123 and allow explicitly specifying generic types on the tearoff. I agree that if we don't implement the tearoffs as resulting in generic functions, that not being able to specify it without using the context type is a large drawback. |
If the "recommended implementation strategy" is to have a static helper function: static List<T> List$filled$tearoff<T>(int count, T value) => List<T>.filled(count, value); and tear off that instead of the constructor (which falls back on existing behavior and gives us precisely the constant/canonicalization behavior we want), then I still want #123, also for the |
The current proposal specifies that when tearing of a constructor from a generic class, the tearoff is always a monomorphic function which has been instantiated, either implicitly via the context type, or via instantiate to bounds. There is an optional extension to allow explicit instantiation via syntax, but no option to tear it off uninstantiated. Example:
It's surprising to me that the choice would be to always instantiate here. If we choose to only support fully instantiated generics, then I think the "possible extension" is almost a must have, since otherwise it's very non-intuitive how to control the torn off type. Having to assign to a context is surprising.
More generally, the fact that
A.named
with no context implicitly instantiates to bounds seems very surprising.I would propose the following alternative: instead, we by default tear off a constructor from a generic class as a generic method.
A concern was raised about how to integrate this with a possible extension to allow generic constructors. This is slightly awkward, but not terrible. Specifically, I would suggest the following:
It is slightly surprising that the type parameters get "uncurried" in this case, but this would be by far a very esoteric corner of the language (if it even comes to exist) and it feels odd to make a more common part of the language (tearoffs of constructors from generic classes) less useful in order to make this hypothetical esoteric corner slightly less awkward.
cc @lrhn @eernstg @munificent @natebosch @jakemac53 @stereotype441
The text was updated successfully, but these errors were encountered: