-
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
Viability of extension constructor tear-offs. #3876
Comments
Or maybe the solution is to add a more general feature, and then let extension constructors use it: Constructors with restricted type parameters (#1899). Basically a constructor can add type arguments to the class name: class Map<K, V> {
factory Map<K extends Comparable<K>, V>.ordered() => SplayTreeMap<K, V>(compareComparable<K>);
} The type parameters in the constructor must have the same number of parameters as the class, and its bounds must be subtypes of the corresponding type parameter bounds of the class. Then you are only allowed to invoke the constructor with type arguments that matches its bounds: var map1 = Map<num, String>.ordered(); // OK
var map2 = Map<Symbol, String>.ordered(); // Error: Symbol <!: Comparable<Symbol>. With that, an extension could declare a constructor of their target class as: class Point<P extends num> {
final P x, P y;
Point(this.x, this.y);
}
extension IntPoint on Point<int> {
Point<C extends int>.diagonalInt(C xy) : this(xy, xy);
} and you an do In practice, I think you'd more often want to have a fixed type, like Possibly: |
@lrhn I got pretty lost in the above, maybe because of the level of abstraction. I don't really understand what problem you're worried about, so maybe a concrete example would help?
I don't know what this means. |
Me too, on re-reading. This issue assumes that whether a static extension with a constructor applies to a specific generic type can depend on the actual type arguments. Which can be inferred. If we do use the instantiated type for applicability, a static extension like static extension NumList<T extends num> on List<T> {
List.singleton(T value) : this.filled(1, value);
} would not be applicable to We still have to "solve for Consider a more complicated example: extension ComparableList<T extends Comparable<T>> on List<T> {
factory List.ordered(Iterable<T> values) => [...values].sort(compareComparable<T>);
} Here Then there are constructor tear-offs. var f = List.ordered; With a raw target type and no context type, the extension constructor is going to be applicable, there is no reason it wouldn't be. If we had a context type Then something like static extension Permutation<T> on Map<T, T> {
factory Map.singleton(T value) => {value: value};
} where we have a non-linear relation, and we want to solve something like: Map<num, int> Function(int) = Map.singleton; That means solving I don't see why I was so worried. If we can't solve, it doesn't apply. If we can solve, we have an instantiation for Or we can say it's always applicable, and then give an error later if we have no conflict and then fail to solve, but then we can include the parameters in the type inference. So, I think my worry above was assuming that we wanted to infer the type parameters to the extension before we could decide whether it was applicable, but that solving for that sometimes required looking at the arguments too, which we shouldn't do before having decided which extension to apply (because we don't want to infer types for arguments more than once). I've had more ideas since then, which could possibly bypass that worry (distinguish raw and instantiated constructor invocations, and for raw ones infer type arguments from a context type as downwards inference, then decide applicability if the type is now instantiated, and keep the raw receiver type if not, and consider the extension constructor applicable. Then if it gets chosen, solve for the actual type arguments during upwards inference.) Or we should just keep it simple and not allow overloading names. It's easier. Cleaner. And we can always loosen it up later, because that would only introduce fewer errors. I'll close this as ... probably ... unfounded worry about a hypothetical approach that we'll very probably not take. |
Assume we can define extension constructors like:
Then we can presumably have extensions on subtypes like:
Invoking such constructors as
C.ext(args)
orC<T1,...,Tn>.ext(args)
is probably possible, if we can solveC<S1, ..., Sn>
<:C<T1, ...,Tn>
forY1
,...,Ym
. If we can't, the constructor is not applicable at that type, and it'll likely be an error, likeList<Symbol>.ordered([#a, #b])
. No solution forT extends Comparable<T>
, soList.ordered
is not applicable at this type. If it was, it would work.The problem comes when we get to constructor tear-offs, like:
Tearing off from
C
is easy, it's just like tearing of an "equivalent function" likeand it's constant and canonicalized, and if instantiated, it's constant if the type arguments are constant.
Tearing off from
E
puts us in a position where we abstract over something we want to solve for.The obvious definition of
C.ext
would be a function equivalent to:The problem with that we check whether an extension constructor is applicable based on the type arguments, and here we don't know the type arguments yet, since we are abstracting over them.
What we need is at least to allow lower bounds on the type parameters of
_tearOff
.That is, we should find a set of "safe bounds",
D1
, ...,Dn
such thatE
is always applicable toC<D1, ..., Dn>
, then use those for the tear-off function:(Can we maybe start with
[B1/Y1,...,Bn/Yn]S1
, ...,[B1/Y1,...,Bn/Yn]S1
, then remove all remaining occurrences ofY1
,...,Ym
by replacing them withNever
/Object?
depending on variance? If that becomes super-bounded, it might still be valid. If it becomes effectivelyNever
, the result won't be useful, but maybe it never was if that's the best solution.)Assume we can find such limits, then we still have to do something for
C<X1, ...,Xn>.ext(params)
, where we try to solve forC<S1,...,Sn>
<:C<X1, ..., Xn>
. There are so many ways that can fail to solve whenX1
,...,Xn
are type variables. The only types that are subtypes of a type variable are type variables that are bounded by it, and bottom types.The cases where it just works are precisely those where each
Si
is a type parameter ofE
(or the occasional bottom type).Any other relation between extension type parameter and
on
-type type argument makes it impossible to abstract over type arguments to extension constructors.(Even a constant type won't work,
extension E2 on List<num>
, but it probably rarely will anyway if we don't have contravariance, sinceList<int>.ctrFromE2(arg)
probably won't be able to create an actualList<int>
without access to the type parameter.)Questions
Should we just say that an extension cannot declare a constructor unless all type arguments to the
on
type (the denoted type declaration that the statics will be on) are type arguments (or subtypes of type arguments) of the extension, put constant types?Or should we make the type arguments to uninstantiated constructor tear-offs be those of the extension.
It's surprising if
Map.self
has typeMap<X, X> Function<X>(X value)
with only one type argument, but it will correspond to anextension SelfMap<X> on Map<X, X> { SelfMap.self(X value) => {value: value}; }
which has only one type parameter.Or something else.
(Which may be why @eernstg's proposal uses
C.name
as the declaration, if that can be used to give it the same type parameters asC
, somehow. Not entirely sure, must read again.)The text was updated successfully, but these errors were encountered: