dart-lang / language

Design of the Dart language
Other
2.66k stars 205 forks source link

Allow constructors to restrict type arguments to class. #1899

Open lrhn opened 3 years ago

lrhn commented 3 years ago

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.

Levi-Lesches commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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

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 commented 3 years ago

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.