Skip to content

Allow constructors to restrict type arguments to class. #1899

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

Open
lrhn opened this issue Oct 11, 2021 · 4 comments
Open

Allow constructors to restrict type arguments to class. #1899

lrhn opened this issue Oct 11, 2021 · 4 comments
Labels
feature Proposed language feature that solves one or more problems

Comments

@lrhn
Copy link
Member

lrhn commented Oct 11, 2021

Currently the constructors a class C<T> { ... } must all accept all the same type arguments as C.

That sometimes leads code authors to restrict the type arguments programatically, like:

class C<T extends Object?> { 
  C(T value) : ...;
  C.nonNull(T value) : assert(null is! T), ....;
}

Here the goal is to have a constructor which only works with a non-nullable type.
Similar problems occur for other subtypes than nullability like:

class TreeSet<E> {
  TreeSet.from(List<E> elements, int Function(E, E) compare): ...
  TreeSet.fromComparable(List<E> elements) : assert(elements is List<Comparable<E>>), ... compare = Comparable.compare;
}

where you can omit the compare function when you know the elements are comparable, but we don't have intersection types, so we can't require elements to be List<E>&List<Comparable<E>> and we can't require E extends Comparable<E>.

So, what if constructors could provide type parameters that are more restrictive than for the class itself:

class TreeSet<E> {
  TreeSet.from(List<E> elements, int Function(E, E) compare): ...
  TreeSet<E extends Comparable<E>>.fromComparable(List<E> elements): ..., compare = Comparable.compare;
}

Type parameters on the class name of a constructor must be valid type arguments to the class itself, and they are implicitly applied. Inside the constructor, the type parameters of the class are not in scope, they're replaced by the type parameters of the constructor. (Or, rather, all constructors have type parameters, if you don't write them, they are copied from the class, and constructors always only see their own type arguments.)

If we ever add extension static members (#723), we might also allow extension constructors.
At that point, it would likely mean that:

extension Ext<T extends num> on List<T> {
  factory Ext.sumList(List<T> values) => [...values, sum<T>(values)];
}

would be allowed as List<int>.sumList([1, 2, 3]) and List<double>.sumList([1.5, 2.4]), but not List<Object>.sumList(...).
That means that we'd introduce the ability to have type-restricted constructors, but only through static extensions. We should then allow you to write the same constructors directly to avoid authors using extensions just for the added flexibility.

@lrhn lrhn added the feature Proposed language feature that solves one or more problems label Oct 11, 2021
@Levi-Lesches
Copy link

Type parameters on the class name of a constructor must be valid type arguments to the class itself, and they are implicitly applied. Inside the constructor, the type parameters of the class are not in scope, they're replaced by the type parameters of the constructor. (Or, rather, all constructors have type parameters, if you don't write them, they are copied from the class, and constructors always only see their own type arguments.)

How would this play with #647, allowing constructors to have type arguments of their own?

@lrhn
Copy link
Member Author

lrhn commented Oct 12, 2021

It would be unrelated to #647. Those constructor-specific extra type arguments go after the name, not after the class, and are not constrained by the class generics.

You would be able to write C<T extends num>.name<X extends T>(X value) : _foo = value;.

@eernstg
Copy link
Member

eernstg commented Oct 14, 2021

In this particular case (where the constructor is named) it would be possible to use a static method rather than a constructor, yielding very similar results:

class TreeSet<E> {
  TreeSet.from(List<E> elements, int Function(E, E) compare);
  static TreeSet<E> fromComparable<E extends Comparable<E>>(List<E> elements) =>
      TreeSet.from(elements, Comparable.compare);
}

void main() {
  TreeSet.fromComparable(['Hello!']); // OK, this works.
}

The main differences would be

  • when type arguments are passed explicitly, we can see that the constructor receives them on the class, and the static method receives them on the method name.
  • the static method doesn't allow new.
  • the constructor could be a non-factory, it could be redirecting, it could be constant.
  • on the other hand, the static method could take a different number of type arguments, and it could have an arbitrary mapping from its type arguments to the type arguments of the returned object (so we could eliminate some redundancies).

So the two approaches are not directly comparable, but there are are a whole range of reasons why this feature would enable certain things that we cannot do today.

@munificent
Copy link
Member

I have on rare occasions wanted this. It's not unreasonable. But I suspect the value is marginal enough that it would be hard to justify the cost of the feature. As Erik notes, you can usually just use a static method, or maybe a subclass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

4 participants