-
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
How do constructor tearoffs of generic classes resolve through type aliases? #1620
Comments
The current proposal deals explicitly with constructor tear-offs through type aliases.
The current proposal would give you:
That is, is generates a function which specializes the generic type alias, not the class. A not-completely instantiated type alias is not a class, it's a generic function from type to class (same as a generic class is not a class, it's a function from types to a class), so the result will be a generic function for the alias function as well, like it would for the generic class. Similarly if we have: typedef Old3<S extends num, T extends num> = New<S, T>;
typedef Old4<S, T> = New<T, S>;
typedef Old5<S, T, U, V> = New<S, T>; we'd get: print(Old3.new.runtimeType); // New<S,T> Function<S extends num, T extends Num>()
print(Old4.new.runtimeType); // New<T, S> Function<S, T>()
print(Old5.new.runtimeType); // New<S, T> Function<S, T, U, V>() The real question is whether any of these are identical with a tear-off of In the current proposal, the The proposal does not try to recognize that So, the current algorithm for tearing off constructors of type aliases is:
So, |
Won't this conflict with allowing constructors to have generics (as in #647)? This would allow having generics before and after class Original<K, V> {
Original.new<E>(K key, V value, List<E> list) { }
}
typedef Alias<S> = Original<S, int>; Some examples of using generic constructors with generic types. Original<String, int>.new // Original<String, int> Function<E>(String, int, List<E>)
Original<String, int>.new<bool> // Original<String, int> Function<bool>(String, int, List<bool>)
Alias<String>.new // Original<String, int> Function<E>(String, int, List<E>)
Alias<String>.new<bool> // Original<String, int> Function<bool>(String, int, List<bool>)
// These two should be the same, but would not be under the above rules
Original.new; // Original<dynamic, dynamic> Function<E>(dynamic, dynamic, List<E>)
Alias.new; // Original<K, int> Function<K> (K, int, List) To reconcile, we should continue to require type arguments for the class to go between the type and |
@Levi-Lesches The current thought is that if it becomes an issue, we can solve it by concatenating the generics: class C<T> {
C.foo<E>() ...
}
var f = C.foo; // <T, E>() => C<T>.foo<E>(); With that, I'd expect: |
I guess my intuition is that it's just an alias, so everything should be the same save for the name. Is there precedent for combining generics? I feel that will be misleading, since it "hides" the true signature of class Good<T> {
Good.new<E>(T value, List<E> list);
}
typedef Bad<T> = Good<T>;
var beforeMigration = Bad.new; // <T, E>(T, List<E>) => Good<T>
var afterMigration = Good.new; // <E>(T, List<E>) => Good<dynamic> Can we instead expect the class generics be separate from the constructor generics? Like so var beforeMigration1 = Bad.new; // <E>(dynamic, List<E>) => Good<dynamic>
var beforeMigration2 = Bad<int>.new; // <E>(int, List<E>) => Good<int>
var beforeMigration3 = Bad.new<String>; // (dynamic, List<String>) => Good<dynamic>
var beforeMigration4 = Bad<int>.new<String>; // (int, List<String>) => Good<int>
var afterMigration1 = Good.new; // <E>(dynamic, List<E>) => Good<dynamic>
var afterMigration2 = Good<int>.new; // <E>(int, List<E>) => Good<int>
var afterMigration3 = Good.new<String>; // (dynamic, List<String>) => Good<dynamic>
var afterMigration4 = Good<int>.new<String>; // (int, List<String>) => Good<int> This way, the only true difference between aliases is just the name. |
@leafpetersen, I think we can close this issue: Proper renames have been specified, and implementations are working or being fixed via other issues. |
In discussions between @eernstg and myself this morning, the following question arose. Consider the following code:
Given the above, the following is valid code, and prints
3
three times:The interpretation of each of the static method calls through the type alias is that we resolve the type alias to a class, and call the static method on the underlying class (regardless of the generic arguments or lack thereof).
Now consider the following program:
This code prints the following:
The interpretation of the use of
Old
etc as a constructor is that inference (in this case via instantiation to bound) is done to fill in the missing type arguments, and the constructor is then invoked.In an upcoming release, we propose to allow constructors to be torn off, and we propose that tearing off a constructor from a generic class can either be done providing type arguments (in which case a monomorphic function is the result) or without providing type arguments (in which case a generic function is the result). Example:
This program is expected (under the current proposal, to print:
Now consider the following program:
What do we expect this to print?
A couple of points:
New<S, T> Function<S, T>()
Old()
and(Old.new)()
will, in general, behave consistently with respect to inference.New<S, T> Function<S, T>()
Old2()
produces aNew<String, int>
, but(Old2.new)()
would produce aNew<dynamic, dynamic>
.New<String, int> Function()
.Old2()
produces aNew<String, int>
and(Old2.new)()
also produces aNew<String, int>
.So it seems clear to me that there is a strong argument for resolving constructor tearoffs through type aliases differently from static methods: we should choose the interpretations from my First and Third points above (even though this is arguably inconsistent with how we resolve static methods).
However, there is an oddity here from a specification perspective in that for
Old
, we are expected to ignore the type arguments toNew
on the RHS of the typedef, but forOld2
we are expected to respect them. This oddity becomes clear when we considerOld1
. What do we expect line 2 to print? Options:New<dynamic, int> Function()
New<String, int> n = Old1()
produces aNew<String, int>
, butNew<String, int> n = (Old1.new)()
would be a static error.New<S, int> Function<S>()
.Old1()
and(Old1.new)()
should produce the same result.New<S, T> Function<S, T>()
.As a bonus exercise, consider:
Some possible interpretations we could take.
First, we could choose to interpret each typedef with a RHS which expands to a class to induce a set of (possibly generic) constructor tearoffs. That is, the declarations:
are essentially considered to induce the declarations:
Second, we could choose to specify directly an interpretation which gives us the result we want. Something along the (very rough) lines of:
C.name
, whereC
is a typedef with type parametersXi
, expand the RHS ofC
fully.This seems tremendously hacky to me, but maybe it can be made cleaner?
Third, we could back off of tearing off constructors as generic methods.
cc @eernstg @lrhn @munificent @natebosch @jakemac53 @stereotype441 @johnniwinther @stefantsov @rakudrama
Thoughts? Other suggestions? Things I'm missing/have mis-stated?
The text was updated successfully, but these errors were encountered: