Sdk: Support generic arguments for (named) constructors

Created on 3 May 2016  路  19Comments  路  Source: dart-lang/sdk

Sometimes constructors need to have generics that don't appear in the class itself in order to fully express their type relationships. new Map.fromIterable() is a good example:

Map.fromIterable(Iterable elements, {K key(element), V value(element)});

If elements has a generic type, the key() and value() callbacks are guaranteed to be passed an argument of that type, but there's currently no way to express that. Using generic types here blocks inference and makes it very difficult to use this API in strong mode. I propose that we be able to declare:

Map.fromIterable<E>(Iterable<E> elements, {K key(E element), V value(E element)});

which would be called like new Map<String, String>.fromIterable<int>(...). This syntax allows the user to omit the generic argument to the constructor while retaining it for the class itself if desired.

area-language core-l language-strong-mode-polish type-enhancement

Most helpful comment

Any updates on this, now that we're post-2.0?

All 19 comments

I have encountered this in other parts of the core libraries as well.

The biggest issue is, that the syntax works much less nicely for unnamed constructors.

I have a few cases too where I could have used that too.

I don't think it's a big problem that it doesn't work for the unnamed constructor. Writing new Foo<int>(...) should be reserved for the plainest way to create a Foo. Do we know of cases where it's a problem?

I haven't encountered one yet, and I agree that this seems like a reasonable restriction.

I think it makes a lot of sense to have this feature. It shouldn't be much extra work on top of all the other kinds of generic routines we have already, and it looks like an oversight to omit it.

The main use for it would be to ensure consistency among type parameters used in the types of ordinary parameters.

It would probably not make much sense for the newly created object to store the actual arguments or anything else which is directly characterized by E (E should then probably have been a class type variable, not a constructor type variable). But I think that the mutually consistent types of ordinary parameters is already a fine justification for having this feature.

@eernstg @floitschG @lrhn @munificent

Is this something we can take on soon? It seems useful.

One funny little thing is that a constructor may use a generic named constructor in a superinitializer, so we need support for super.theName<Some, Type, Arguments>(some, value, arguments) as well (there's no guarantee that the superinitializer takes the same type arguments, or even the same number of type arguments).

Given that there is no return value from a constructor we generally won't have a contextual expectation available during type inference for these type arguments, but the type arguments would presumably be used to "fill in some coordinated blanks" in the types of value arguments, so type inference is likely to be able to provide the type arguments.

So there will be some corners of this feature which are new, but probably no show-stoppers.

On minimal thought, I would expect that if we have:

class B<T> extends A<F<T>> {
  B.make<S>(...) { super.make(...); };
}

then you could use A<F<T>> as the contextual expectation for the super call? I might be missing some details though - definitely needs to be worked through.

The starting point is that we are adding a new construct: superCallOrFieldInitializer in the grammar would need to be modified to allow for passing actual type arguments. We might need to answer new questions in relation to that, and I was just checking out whether we're likely to have new and interesting problems when we implement this feature.

It's about the type arguments of a named constructor itself, not the type arguments of the enclosing class. E.g., in super.make below we pass <int> (or we could have it inferred):

class A {
  int x;
  A.make<T>(String s, int Function(T) f, T Function(String) g) {
    x = f(g(s));
  }
}

class B extends A {
  B.make(String s): super.make<int>(s, (s) => s.length, (i) => i);
}

I noted that we don't have a return type since this is a constructor, and the type arguments of the constructor itself would not be able to occur in the type of the newly constructed object, and hence we couldn't use a return type nor the type of this during inference of those actual type arguments.

In practice, I expect these type arguments to be used to bridge some gaps in the typing of the actual arguments, like in the example. This seems to imply that "transfer of information from one argument to another" will be important for this particular kind of inference.

However, my conclusion was that this isn't actually new (the same difficulties can arise in concrete examples today), so we should probably just get started. ;-)

I'm in favor of getting this done.

As Natalie mentioned: we have cases where we need those in the core library, and there shouldn't be any big show-stoppers.

I would really like this feature too. Map.fromIterable() is pretty frequently used, and the user experience is decidedly subpar right now.

Similar #30041

Assuming this doesn't make it into Dart 2.0, it wouldn't be considered a _breaking_ language change, so that it could be added soon after, correct? Like Dart 2.1? I've gotten many questions about this, mostly regarding Map.fromIterable().

Don't hold me to this:

  • I don't think it's a breaking change to the language, though there may be a weird corner of the grammar I'm not considering. I believe, though, that the combination of generic methods and optional new in Dart 2.0 means that we've already absorbed the grammatical breakage we'd need for generic constructors.

  • I believe it probably is a (minor) breaking change to the core libraries to use generic constructors for things like Map.fromIterable(). Consider:

    List<String> strings = [];
    doubleInt(int i) => i * 2;
    Map.fromIterable(strings, key: doubleInt);
    

    I think this (not very useful) code would run in Dart 2.0 and fail statically if Map.fromIterable became generic when it tries to infer T.

It's not a breaking change to the language. It's just a case of a feature not making it into Dart 2.0, like a lot of other good features. It's pretty high on my list of annoyances to address ASAP.

Constructors are basically static methods, and we don't have tear-offs of them yet, so any change to the signature that allows all existing calls should be safe.

The example above ... is a breakage I would be willing to allow.
It's so badly typed that it's not meaningful. The fact that we couldn't catch the type error doesn't make the program valid, just lucky. If we changed the constructor to:

Map.fromIterable<T>(Iterable<T> elements, {K key(T element), V value(T
element)})

then any good code should still work. It took an empty list above to get something mis-typed through without causing a runtime error.

Any updates on this, now that we're post-2.0?

Can you be explicit and say new Map<Foo, Bar>.fromIterable<Baz>(...) ?

The support of generic arguments for named constructors is also needed in this case:

abstract class Filter<T> {
  Filter();

  factory Filter.type/*<C extends T>*/() = _TypeFilter<C, T>; // no other way to specify C here :(

  bool allow(T t);
}

class _TypeFilter<C extends T, T> extends Filter<T> {
  @override
  bool allow(T t) => t is C;
}

Any updates on this, now that we're post-2.0?

No update. We're currently hard at work on non-nullable types and extension methods, so we probablty won't get to this until those are done.

Was this page helpful?
0 / 5 - 0 ratings