Constructor specific generics
A broader version of #276
Problem
Currently, generic parameters are defined only on the class level.
But in some situations, a specific constructor may want extra generic parameters (which don't have an impact on runtimeType).
Consumer vs Consumer2 or Consumer vs Selector from provider are good examples of such use-case. Same thing with ProxyProvider vs ProxyProvider2
Currently, due to the lack of constructor specific generic parameters, the different options are split into different classes. But ultimately, the implementation of these different classes is the same.
An abstract example of how these duplicates currently work is:
abstract class _BaseClass {
_BaseClass(this.callback);
final Widget Function(BuildContext context) callback;
}
class Concrete1<T> extends _BaseClass {
Concrete1(Widget callback(BuildContext c, T value))
: super((c) => callback(c, doSomething<T>(c));
}
class Concrete2<T, T2> extends _BaseClass {
Concrete2(Widget callback(BuildContext c, T value, T2 value))
: super((c) => callback(c, doSomething<T>(c), doSomethingElse<T2>(c));
}
Proposal
The idea of this proposal is to allow named constructors to have extra generic parameters:
class MyClass {
MyClass();
MyClass.named<B>(B b);
}
void main() {
MyClass myClass = MyClass();
myClass = MyClass.named<int>(42);
}
As such, the snippet from "problem" example could become:
class Concrete {
Concrete.foo<T>(Widget callback(BuildContext c, T value))
: this._((c) => callback(c, doSomething<T>(c));
Concrete.bar<T, T2>(Widget callback(BuildContext c, T value, T2 value))
: this._((c) => callback(c, doSomething<T>(c), doSomethingElse<T2>(c));
Concrete._(this.callback);
final Widget Function(BuildContext context) callback;
}
This improves auto-complete and reduce pointless duplicates.
See also https://github.com/dart-lang/sdk/issues/30041, https://github.com/dart-lang/sdk/issues/26392, and https://github.com/dart-lang/sdk/issues/26391.
None of them have been moved to the language repo yet, so I guess this issue can represent them all :)
any update on this issue?
No update, sorry. We're almost entirely focused on null safety right now.
Any movement on this one as mentioned earlier null safety is deployed.
Not yet.
In the meantime we could note that the examples in this issue are covered pretty well by static methods (note that the call sites are unchanged, we just replace a generic constructor by a generic static method):
abstract class _BaseClass {
_BaseClass(this.callback);
final Widget Function(BuildContext context) callback;
}
class Concrete1<T> extends _BaseClass {
Concrete1(Widget callback(BuildContext c, T value))
: super((c) => callback(c, doSomething<T>(c)));
}
class Concrete2<T, T2> extends _BaseClass {
Concrete2(Widget callback(BuildContext c, T value, T2 value2))
: super((c) => callback(c, doSomething<T>(c), doSomethingElse<T2>(c)));
}
class MyClass {
MyClass();
static MyClass named<B>(B b) {}
}
void main() {
MyClass myClass = MyClass();
myClass = MyClass.named<int>(42);
}
class Concrete {
static Concrete foo<T>(Widget callback(BuildContext c, T value)) =>
Concrete._((c) => callback(c, doSomething<T>(c)));
static Concrete bar<T, T2>(
Widget callback(BuildContext c, T value, T2 value2)) =>
Concrete._((c) => callback(c, doSomething<T>(c), doSomethingElse<T2>(c)));
Concrete._(this.callback);
final Widget Function(BuildContext context) callback;
}
// Glue code, needed to make the rest compile.
class Widget {}
class BuildContext {}
doSomething<X>(dynamic argument) {}
doSomethingElse<X>(dynamic argument) {}
This doesn't cover all cases: A static method cannot return a constant object, so it won't work to emulate a const constructor. Also, the static method must receive its type arguments together if both the class and the constructor are generic:
class A<X> {
final X x;
A._(this.x);
A.named<Y>(Y y, X Function(Y) f): this._(f(y));
}
void main() {
A<bool>.named<int>(42, (x) => x.isEven);
// With a static method, it would be `A.named<bool, int>(42, (x) => x.isEven)`.
}
So the real generic constructors are still worth exploring.
@eernstg sorry for the delay in responding. Dealing with other fires.
Indeed, I've used statics for every other case but this one. But this is just an example. I'm constantly fighting this, and this one doesn't refactor out the issue without having to create endless permutations and combinations of statics (which is true of every other case as well)
Heck, even this won't pick up the right type:
return ViewModelWidget<PostsModel>(
viewModel: viewModel,
....
builder: (context, viewModel, previousEvent, event) { },
}
The generic is hard coded there because dart won't pick up the generic type properly and viewModel in the builder parameters is "Object?" instead of PostsModel, even though viewModel property in the constructor is the generic type!
Here's the widget in question:
class ViewModelWidget<TViewModel extends ViewModel> extends StatefulWidget {
final TViewModel viewModel;
final Widget Function(BuildContext context, TViewModel viewModel,
BaseEvent? previousEvent, BaseEvent? event) builder;
final FutureOr<void> Function() onLoad;
ViewModelWidget({
required this.viewModel,
required this.builder,
required this.onLoad,
});
...
}
And that's just one in a long line of Dart's generics support getting confused for no reason when it's explicitly and obviously set to an exact property and should absolutely be known. (and in this case, the property isn't even a generic itself, nor is it a list, it's just a class that extends ViewModel per the definition.
Now that constructor tear-offs are coming, is this still the syntax being considered? I pulled it from https://github.com/dart-lang/language/issues/1586#issuecomment-819427711.
class Foo<T> {
Foo.new<E>(T value, List<E> list);
}
Foo.new; // <E>(dynamic, List<E>) => Foo<dynamic>
Foo.new<bool>; // (dynamic, List<bool>) => Foo<dynamic>
Foo<int>.new; // <E>(int, List<E>) => Foo<String, int>
Foo<int>.new<bool>; // (int, List<bool>) => Foo<String, int>
@Levi-Lesches, you can find the syntax for the constructor-tearoffs feature bundle (it's constructor tearoffs plus several other things) here.
We don't have generic constructors (yet, at least), so if you're thinking about Dart-with-constructor-tearoffs then you can't declare <E> for the constructor Foo, and hence you also can't pass actual type arguments to it (as in Foo.new<bool> or Foo<int>.new<bool>). We haven't discussed how to combine the upcoming constructor tearoffs feature with the hypothetical generic constructors feature, but if we do introduce generic constructors then the syntax might well end up being the one that you mention.
@JohnGalt1717 wrote:
viewModel in the builder parameters is "Object?" instead of PostsModel
I can't see in detail what's going on here. Type inference can not succeed and select the actual type argument Object? in a creation of an instance of ViewModelWidget, because that violates the bound.
That said, let me note that Dart type inference does not transfer information from one actual argument to another, and we're aware of the fact that there are some heuristic methods (e.g., in C#) that we might be able to adapt for Dart.
So if the type inference fails, and the analyzer or compiler reports that it tried to use Object? then that might be because of constraints that are imposed on the type parameter T by some of the other actual arguments.
In any case, this doesn't seem to fit here, because this issue is about generic constructors. This is a mechanism that Dart currently doesn't have, but it might be added in the future. But the ViewModelWidget example does not use any generic constructors, nor does it even hint at them.
@eernstg I'd argue that virtually all of generic constructor requests is because of the failure of Dart to properly apply the obvious.
Take the following simple example of Dart's failure:
class SomeClass<T extends double> {
final T someDoubleThing;
SomeClass({required this.someDoubleThing, required void Function(T doubleThing) someFunction,});
}
Dart will NOT know the type of T doubleThing in someFunction.
Yet it's explicitly obvious exactly what T is in the function because T is defined by this.someDoubleThing going in when you create it. But dart insists that it's dynamic unless you explicitly define SomeClass with the generic type specified. AND it doesn't error as it should on undefined and uninferred generics.
If you just fixed this one thing, which to me is a bug because this isn't even inference, T is known by the parameter which is explicitly set in the constructor for the class and works in ALL other cases in code, you'd get rid of 99% of the reason why you'd ever need generic constructors.
The only other reason for constructors is when the types are inferred by the result of a parameter, which could be done if the function return is done in a bodyless function but not if it has a body. Fix that, and there would basically be no need for generic constructors.
Oh, it looks like I never got around to respond to this. Here we go:
@JohnGalt1717 wrote:
virtually all of generic constructor requests is because of the failure of Dart to properly apply the obvious.
I changed the bound from double to A and added a couple of classes such that we can use the bound in a non-trivial way without using the type Never. Here's the example again, with those changes:
class A {}
class B extends A {}
class C<T extends A> {
final T t;
C({required this.t, required void Function(T) fun}) {
fun(t); // Just demonstrate that `fun` can be used.
}
}
void main() {
C(t: B(), fun: (x) => x.expectStaticType<Exactly<B>>());
}
typedef Exactly<X> = X Function(X);
extension<X> on X {
X expectStaticType<Y extends Exactly<X>>() => this;
}
This shows that the inferred parameter type of the function literal is B.
Dart will NOT know the type of T doubleThing in someFunction.
This used to be true, but the type inference algorithm of Dart was enhanced with the so-called horizontal inference in Dart 2.18, and this enables information to be transferred from one actual argument (here t, formerly known as someDoubleThing) to another (the function literal).
This is excellent!