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

Support generic arguments for (named) constructors #1510

Closed
nex3 opened this issue May 3, 2016 · 22 comments
Closed

Support generic arguments for (named) constructors #1510

nex3 opened this issue May 3, 2016 · 22 comments

Comments

@nex3
Copy link
Member

nex3 commented May 3, 2016

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.

@floitschG
Copy link

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

@floitschG
Copy link

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

@lrhn
Copy link
Member

lrhn commented May 4, 2016

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?

@floitschG
Copy link

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

@eernstg
Copy link
Member

eernstg commented May 9, 2016

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.

@leafpetersen
Copy link
Member

@eernstg @floitschG @lrhn @munificent

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

@eernstg
Copy link
Member

eernstg commented May 16, 2017

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.

@leafpetersen
Copy link
Member

leafpetersen commented May 16, 2017

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.

@eernstg
Copy link
Member

eernstg commented May 17, 2017

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. ;-)

@floitschG
Copy link

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.

@munificent
Copy link
Member

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

@zoechi
Copy link

zoechi commented Aug 3, 2017

Similar dart-lang/sdk#30041

@srawlins
Copy link
Member

srawlins commented Feb 9, 2018

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().

@munificent
Copy link
Member

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.

@lrhn
Copy link
Member

lrhn commented Feb 10, 2018

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.

@joshua-i
Copy link

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

@Hixie
Copy link

Hixie commented Sep 10, 2018

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

@tlserver
Copy link

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;
}

@munificent
Copy link
Member

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.

@mit-mit mit-mit transferred this issue from dart-lang/sdk Mar 11, 2021
@mit-mit
Copy link
Member

mit-mit commented Mar 11, 2021

Moving to the main language repo

@eernstg
Copy link
Member

eernstg commented Mar 11, 2021

Note that this issue (when located in the SDK repo) was already mentioned as one of the existing proposals for exactly this feature in #647 (comment). So we may wish to close one of these as a duplicate of the other one, to avoid fragmenting the discussion and the support.

@mit-mit
Copy link
Member

mit-mit commented Mar 11, 2021

Good point, let's continue the discussion in #647

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