-
Notifications
You must be signed in to change notification settings - Fork 213
Do explicit partial generic instantiation generalize to function typed expressions? #1604
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
Comments
Ad Q1: I was not planning to. It's orthogonal to explicit instantiation, and it's probably not cost-free. We've been doing without it until now. Being able to specialize a function declaration at tear-off means that we can reuse the same specialization function every time. We only need one It matters that we have per-function helpers, not per-instantiation-point wrappers. void foo<T>(T x, [T y]) {}
void Function(int) f = foo;
print(f.runtimeType); // void Function(int, [int]) -- *NOT* void Function(int) Only specializing declaration tear-offs means we can't specialize a function expression at all, it's only for named declarations. If we choose to specialize a function value, then that changes. The following looks like something which should then work: S Function(S) specialize<S>(T Function<T>(T) f) => f; // general instantiation of generic function typed expression.
X id<X>(X value) => value;
int Function(int) intId = specialize(id);
X defaulter<X>(X? value, [X? defaultValue]) => value ?? defaultValue ?? throw ArgumentError("no default");
intId = specialize(defaulter);
print(intId.runtimeType); // int Function(int?, [int?]) The The question is how to do that effectively when all you have is a function value and a static type. We could let every function value carry around its own "specializer" information. It would be harder to tree-shake if we instantiate any compatibly typed function value anywhere in the program. Probably doable, but with an overhead. Ad Q2: If yes to Q1, then yes here too. It should work for any arbitrary function-typed expression, which Generally, an extension method |
Someone (@lrhn perhaps?) mentioned in the language meeting today that in terms of expressibility, the user doesn't actually lose anything if we restrict I tried it: main() {
S Function(S) specialize<S>(T Function<T>(T) f) => f.call;
X id<X>(X value) => value;
int Function(int) intId = specialize(id);
X defaulter<X>(X? value, [X? defaultValue]) => value ?? defaultValue ?? (throw ArgumentError("no default"));
intId = specialize(defaulter);
print(intId.runtimeType);
} and I'm sorry to say that it exposed a behavioral difference between CFE and analyzer. The analyzer says this code has no errors, but the CFE says:
(I'll file an issue in the SDK repo about this behavioral difference, and once we decide whether the analyzer or the CFE is "correct" we can assign it to the appropriate team 😃) After experimenting a bit, I think that the CFE only allows generic instantiation of Which is not terribly surprising after our discussion today about this stuff being actually implemented through a hidden Which makes me think that: I'm not sure where we go from here, though. |
(Further musings) I keep thinking back to @leafpetersen's mental model of generic instantiation as syntactic sugar for an eta expansion (i.e. if Obviously it would be a breaking change in theory, but how breaking would it be in practice? It sounds from our discussion today (and from the bug I discovered above) like the only user-visible effect of the change would be that when tearing off a method via a base class and applying generic arguments, the resulting method has a runtime type that matches its static type at the site of the tear off, so the user couldn't later downcast it to a more specific type and make use of e.g. extra optional arguments. My suspicion is that the number of people who do that in practice is approximately zero. And I think the implementation cost could be kept pretty low, because two instantiations that act on the same generic function type could share a common implementation, e.g. we would compile this: foo(T Function<T>(T) a, T Function<T>(T) b) {
int Function<int> aInt = a;
String Function<String> bString = b;
} into this: U Function(U) instantiate$T_Function_T<U>(T Function<T>(T) f) => (u) => f<U>(u);
foo(T Function<T>(T) a, T Function<T>(T) b) {
int Function<int> aInt = instantiate$T_Function_T<int>(a);
String Function<String> bString = instantiate$T_Function_T<String>(b);
} |
When considering whether to generalize generic function and method instantiation to 'generic function object instantiation' (such that it's applicable to every function object, not just to a term that statically resolves to a specific function or method), we seem to have three obvious choices available:
|
I have been checking how we canonicalize/make equal tear-offs of the same function. In short: Not very consistently. See the code on Dartpad.
(Dart2js is run on DartPad, VM locally on release 2.12.4). Dart2js either canonicalizes or it has no equality at all. The VM recognizes equality of instance method instantiated tear-offs, but not any other kind. |
We decided not to generalize this, and also to add spec language making it explicit that you cannot implicitly instantiate the .call method of an arbitrary function. |
Implicit instantiation of generic functions, e.g:
are restricted to cases where the thing being partially instantiated is an identifier which names a function or method declaration. It may not be an arbitrary expression (e.g. a local variable).
Question 1: For the explicit variant, do we intend to relax this (for implicit also?)?
Question 2: If so, we we allow explicit instantiation of
this
(for implicit and/or explicit)?cc @lrhn @eernstg @munificent @natebosch @jakemac53 @stereotype441
The text was updated successfully, but these errors were encountered: