Skip to content
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

Specialize interfaces for specific generic types on a class #276

Open
natebosch opened this issue Mar 18, 2019 · 4 comments
Open

Specialize interfaces for specific generic types on a class #276

natebosch opened this issue Mar 18, 2019 · 4 comments

Comments

@natebosch
Copy link
Member

Solution for #275

Add a way to put conditions on the existence of a constructor or method depending on the specific generic type of a class. I think this would apply to the static type at the callsite.

This would let us statically prevent certain calls that would otherwise have to throw at runtime.

Separately, in the discussion for static extension methods we learned that it may be possible to write an extension that is specialized based on the generic type. This gives us a capability with extension methods that isn't possible with instance methods. Adding instance methods conditional on the specific generic type would bring this same power without needing to separate out the implementation to a extension.

Straw man:

class List<E> {
  List.filled(int size, E fill);
  List([int size]) if E extends Object;

  num sum() if E extends num;
}

void main() {
  var list1 = List<int>.filled(5, 1);
  var list2 = List<int>(1); // static error. "The class 'List<int>' does not have a default constructor."
  print(list1.sum());
  var list3 = ["Hello"];
  print(list3.sum()); // static error. "The method 'sum' isn't defined for the class 'List<String>'."
}
@munificent
Copy link
Member

munificent commented Mar 18, 2019

For the first example, generic constructors would cover it, I think:

class List<E> {
  List<T extends Object>([int size]);
}

The sum() example should be covered by extension methods:

extension NumericListMethods<T extends num> on List<T> {
  T sum() => ...
}

@natebosch
Copy link
Member Author

generic constructors would cover it, I think:

I don't know if we have a concrete proposal yet, but my understanding had been that generic constructor type arguments are a separate list from the type's generic argument list. They would go to the right of the constructor name and not to the right of the type name.

If we can solve this problem with allowing the syntax you proposed that would be great.

should be covered by extension methods

Yeah this use case is definitely less necessary and I can't think of a time I'd need it personally - although I can imagine that someone may want the capability for instance methods instead of extension methods so they can be overridden.

@lrhn
Copy link
Member

lrhn commented Jun 19, 2019

What Bob is saying here is not "generic constructors" as we normally use the term. I agree that this is a new feature,more like generically constrained constructors. Here it's a constructor for List<E> (a type which allows any type as type argument) where the constructor can't be used with all values of E. It adds an extra constraint to the type parameter, which simultaneously constrains the return type of the constructor.
This would be very neat. We have a lot of cases where we may take a comparator, and if not, the type parameter needs to be comparable. That could be split into two different constructors, like SplayTreeMap<T extends Comparable<T>>() and SplayTreeMap<T>.compare(int Function(T, T) compare);.

The List([int size]) actually needs more than a constraint, it needs a return type:

List<T?> List<T>([int size]) : ...

because it needs to return a nullable type.

The "member only exists if type parameter is something" feature is more worrysome to me.
I think it could work, after all we treat List<int> as a subtype of List<Object>, so it would make some sort of sense, type-wise, if the former could have more members than the latter.
It's just not how we usually think of interfaces.

If you do List<Object> o = <int>[1];, then the static type of o would not have a sum member, but the run-time value would. That would work.

More problematic would be:

class C<T> {
   int foo() if T extends Foo => 42
}
class D<T extends Bar> extends C<T> {
   String foo() => "foo";
}

I guess this declaration would not be valid because it is possible that some class might implement both Foo and Bar, and then the interface is inconsistent.

We would have to treat the conditional member as always there for subclassing. That's not unreasonable. I think it could work.
In practice, the member could be there always, and throw if it gets called on a type parameter where it doesn't apply. It would be removed from the static type, so you get compile-time errors if you try to call the member at a point where it isn't there.

A dynamic call should work if the list has a num element type (non-null num with NNBD).
So, dynamic x = [1]; x.sum(); should work. What should happen for dynamic x = <Object>[]; x.sum();. A no-such-method or more specific error? I'd go the method actually being there.

So the example above would be implemented as:

class List<E> {
  ...
  num sum() {
    if (this is! List<num>) throw UnsupportedError("sum on List<$E>");
    // E promoted to E&num.
    ...
  }
}

but an invocation of List<Object> o; o.sum(); would be rejected statically because List<Object> doesn't support sum. It's like covariant in that we do static checking where possible, but fall back on run-time checking if necessary (like for dynamic calls).

@natebosch
Copy link
Member Author

Prior discussion of something similar at dart-lang/sdk#32120 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants