-
Notifications
You must be signed in to change notification settings - Fork 209
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
Infer generic type parameters based on the declaring type #620
Comments
I'm really not sure what you're asking for here. Are you suggesting that given an example like: class BlocBuilder<TBloc extends Bloc<dynamic,TState>> {} in the case that |
i think a simpler way to express this is to make the or another way of solving this is to keep class BlocBuilder<TBloc extends Bloc<dynamic,TState>,TState> {} as it is, but make this a valid definition by inferring TState BlocBuilder<TestBloc>() |
Ok, leaving aside the question of implicitly defining type parameters which I think is problematic, the question of inferring missing type arguments seems more reasonable. I think you're proposing that if type arguments are left off (as in Without explicit per variable syntax for the elided variables, we'd need to restrict this to trailing arguments. Otherwise you don't know which parameters to match up the arguments provided against. |
I am not sure having to explicitly state a place holder for inferred types is necessary, the compiler can just check if the type argument has been assigned before to infer it. the checking part already happens though, e. g. BlocBuilder<TestBloc, int> will give a compile-time error, since int is not of type String. |
It is necessary if you want to allow arguments other than the trailing arguments to be omitted. |
do you mean this case ? class BlocBuilder<TBloc extends Bloc<TEvent,TState>,TEvent,TState,TSomethingElse> {}
BlocBuilder<TestBloc,TestSomethingElse>() this makes sense, since the compiler isn't sure whether you want to check so i think we have to allow only trailing arguments to be omitted |
we can even add some modifier at the declaration to specify the type as implicit, this way we don't have to care where is it located, e.g. class BlocBuilder<TBloc extends Bloc<dynamic,TState>, implicit TState> making this a valid declaration : class BlocBuilder<implicit TEvent, TBloc extends Bloc<TEvent,TState>,implicit TState,TSomethingElse> {}
BlocBuilder<TestBloc,TestSomethingElse>() |
Thinking about this some more, I wonder if this isn't actually more of a request for patterns rather than inference. That is, something more like (inventing some syntax): class Bloc<TEvent,TState> {}
// BlocBuilder only takes one type argument, but it pattern matches against that
// argument and binds TState to the second argument of TBloc.
class BlocBuilder<TBloc as Bloc<dynamic,Type TState>> {}
class TestBloc extends Bloc<String,String> {}
class TestBlocBuilder extends BlocBuilder<TestBloc> {} cc @munificent I wonder if this is another pattern matching use case to consider? I do feel like I've seen this style of code before, where a type variable exists just to give a name to a part of another type variable. |
is there any update on this ? I think @leafpetersen 's proposal is pretty good |
We could use type patterns for this, cf. #170: class BlocBuilder<TBloc extends Bloc<dynamic, var TState>> {} This would then perform static type pattern matching only (there is no need for a match at run time, because the actual type arguments will be denotable types at instance creation, and the construct would be desugared to have two type arguments). The type parameters that are introduced by matching would have bounds derived from the context of the pattern (so There are two perspectives on this, and I think that both amount to a useful feature:
So this would actually be quite nice! |
For now, we can make a custom method to partially solve this issue: abstact class Foo<Param> {
R capture<R>(R cb<T>()) {
return cb<Param>();
}
} Which can then be used this way: void printGenericValue<T extends Foo<dynamic>>(T value) {
print(value.capture(<Param>() => Param));
}
class Bar extends Foo<int> {}
void main() {
printGenericValue(Bar()); // prints `int`
} This is not ideal, but unblock the situations where we have control over the base class. On the other hand, this issue is still important for classes where we don't have the ability to modify the implementation class, such as:
It's also important for situations where we want that generic parameter to be the type of the result of a function: Param example<T extends Foo<var Param>>() {
...
} |
@rrousselGit thanks for the suggestion but unless I'm misunderstanding I don't think this workaround applies when you have a generic parameter which extends a class that has another generic parameter. class Foo<T> {
const Foo(this.value);
final T value;
}
class Bar<T extends Foo<S>, S> {
Bar({this.foo, this.baz}) {
baz(foo.value);
}
final T foo;
void Function(S s) baz;
}
void main() {
final foo = Foo<int>(0);
final bar = Bar(
foo: foo,
baz: (s) {
// s is dynamic
},
);
} In the above example, I can't seem to force Dart to resolve the type of Bar<Foo, int>(
foo: foo,
baz: (s) {
// s is an int
},
); |
I hit something similar today R watch<T extends ValueListenable<R>, R>({String instanceName}) =>
state.watch<T, T>(instanceName, null).value; When only providing |
@escamoteur I just hit that too and it's not the first time. |
I am looking for the same thing. Here is a simple example that could be infered but throws on runtime
Inference shows that IterableManager is of Best imo would be to allow the generic to be declared inside extend like |
You're encountering the issue that type inference in Dart does not support flow of information from one actual argument to another, cf. #731. By the way, can you give a hint about why you wouldn't be happy with the following?: class IterableManager<T> {
IterableManager({required this.iterable});
final Iterable<T> iterable;
T getIndex(int index) => iterable.elementAt(index);
}
void main() {
var manager = IterableManager(iterable: List.generate(10, (index) => index));
double index = manager.getIndex(3);
} |
@eernstg this has nothing at all to do with #731. There are no arguments here for information to flow through. There are other things you can try to do to incorporate information from the bounds in order to derive a "tighter" constraint, but when I've looked into this, I've found that no solution dominates: for any heuristic you pick, you make some patterns work, and others fail. For these kind of patterns where you're trying to "program" the generics in order to extract sub-components of inferred types, I believe the solution that I pointed to in my comment above is probably the right direction. |
Right, thanks! |
I have this problem all the time. For instance: class ModularRoute<TPageParameters extends PageParameters,
TModularPage extends ModularPage<TPageParameters>>
extends BaseModularRoute<TPageParameters, TModularPage> {
ModularRoute({
required BaseModule module,
required String route,
required TModularPage Function(Map<String, String?> params) createPage,
FutureOr<bool> Function(
ModularHistory route,
ModularRouterDelegate delegate,
)?
guard,
bool overrideModuleGuard = false,
}) : super(
module: module,
route: route,
createPage: createPage,
guard: guard,
overrideModuleGuard: overrideModuleGuard,
);
} Dart completely fails to infer the types even though they're obviously provided in the parameters of the constructor. (i.e. since TModularPage is defined and includes TPageParameters in what it extends, then we know both what TModularPage is and TPageParameters based on the createPage method if the createPage function is in lamda notation. But let's say that it isn't in lamda notation or you have a case where you can't infer the types for the generic. This class would be better expressed as: class ModularRoute<TModularPage extends ModularPage<TPageParameters extends PageParameters>> extends BaseModularPage<TModularPage> {} (BaseModularPage has the same problem) Thus used like this: final route = ModularRoute<SomeModularPage>(...); This explicitly states that there are 2 generics that define this class that can be used in the class AND that they can both be found by the single definition because the extension is itself is limited to a generic type that defines the other parameter. We would then be able to use TModularPage and TPageParameters strongly throughout our code without repeating ourselves over and over again for something that is clearly in the generic definition this way. From what I can tell, all other options above either only work if you know the types (which you don't necessarily know if it's being derived from a function) or you add other verbosity to your creation of the object. |
Many years waiting for this, with many duplicate issues posted. Any changes we see a solution in the near future? |
It has been sooo long that I forgot how bad the Flutter community needs this. |
I needed to select class by its generic type. Here is code based on @rrousselGit solution class Par {}
class Par1 extends Par {}
class Par2 extends Par {}
class Api<TSub> {
R _capture<R>(R Function<TSub>() cb) => cb<TSub>();
getItemRuntimeType() => _capture(<P>() => P);
@override
toString() => 'Base Api for ${getItemRuntimeType()}';
}
class Par1Api extends Api<Par1> {
@override
toString() => 'Par1Api';
}
class Par2Api extends Api<Par2> {
@override
toString() => 'Par2Api';
}
void main() {
check(Par1(), [Par1Api(), Par2Api()]); // targets: [Par1Api]
check(Par2(), [Par1Api(), Par2Api()]); // targets: [Par2Api]
check(Par1(), [Par1Api(), Par2Api(), Api<Par1>(), Api<Par2>()]); // targets: [Par1Api, Base Api for Par1]
check(Par2(), [Par1Api(), Par2Api(), Api<Par1>(), Api<Par2>()]); // targets: [Par1Api, Base Api for Par1]
check(Par1(), [Api<Par1>(), Api<Par2>()]); // targets: [Base Api for Par1]
check(Par2(), [Api<Par1>(), Api<Par2>()]); // targets: [Base Api for Par2]
}
void check(Par target, List<Api> checkers) {
final targetClass =
checkers.where((e) => e.getItemRuntimeType() == target.runtimeType);
print('targets: [${targetClass.map((t) => t.toString()).join(', ')}]');
} |
As a riverpod user, this is so much needed. It always infers // inferred as NotifierProviderImpl<SettingsNotifier, SettingsState>
final settingsProvider = NotifierProvider<SettingsNotifier, SettingsState>(() {
return SettingsNotifier();
});
// inferred as NotifierProviderImpl<SettingsNotifier, dynamic>
final settingsProvider2 = NotifierProvider(() {
return SettingsNotifier();
});
class SettingsNotifier extends Notifier<SettingsState> {
// ...
} You don't notice the missing generics right away, but as soon as you read it with |
Note that there are various ways to catch things Numerous lints offer a warning if type inference defaults to dynamic. There are also some "strict" options such as strict-raw-types which can help. |
The issue is that you're adding endless extra code that isn't needed. Dart should immediately know that this isn't dynamic and what the type is. I shouldn't have to repeat myself (i.e. this violates DRY). And while strict raw types catches this most of the time, there are cases where it won't and you're creating runtime bugs as a result. The language server and dart itself should catch this at compile time, know the type and validate everything strongly because there is absolutely no posibility that it can be anything other than the obvious. |
The ability to perform pattern matching on type arguments could be a somewhat complex feature, so maybe we should consider a simpler approach. I proposed a "type function" based approach in #3324. Using that approach, we could handle the original example here as follows (with a slight enhancement, because we need to use // We can write this today.
class Bloc<TEvent, TState> {}
class BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> {
TState get g => ...;
}
// We would like to omit the type argument for `TState` in client code, so we use the new feature.
class Bloc<TEvent, TState> {}
class BlocBuilder<TBloc extends Bloc> {
ImplementsAt2<TBloc, Bloc> get g => ...;
} The point is simply that we can express the type which is the type argument In other words, we have a type @Tienisto wrote:
I assume the declaration would this one (found here): typedef NotifierProvider<NotifierT extends Notifier<T>, T>
= NotifierProviderImpl<NotifierT, T>; We would then use this instead: typedef NotifierProvider<NotifierT extends Notifier>
= NotifierProviderImpl<NotifierT, ImplementsAt1<NotifierT, Notifier>>; which would allow this // We can provide the type argument explicitly.
final settingsProvider = NotifierProvider<SettingsNotifier>(() => SettingsNotifier());
// But it is already inferred as `SettingsNotifier`, which should still work.
final settingsProvider2 = NotifierProvider(() => SettingsNotifier()); |
@eernstg Regarding your Riverpod / provider proposal: We cannot reduce the generic count to one because both generic types are needed. We need to access the notifier (1) and we also need to access its state (2). final a = ref.read(settingsProvider.notifier); // returns NotifierT
final b = ref.read(settingsProvider); // returns T if |
The proposal in #3324 allows us to do exactly that: We declare In particular, final a = ref.read(settingsProvider.notifier); // `a` has inferred type `NotifierT`.
final b = ref.read(settingsProvider); // `b` has inferred type `ImplementsAt1<NotifierT, Notifier>`. So the inferred type of I mentioned the possibility that we could combine this with the introduction of optional type parameters. This would give you a little more control, because we could choose "T" to have a value which is not minimal (but which would still ensure that all bounds are satisfied): typedef NotifierProvider<NotifierT extends Notifier<T>, T = ImplementsAt1<NotifierT, Notifier>>
= NotifierProviderImpl<NotifierT, T>; If we do it like that then we will have access to |
Sounds good to me. Honestly I don't think it's that common is user code. It's likely mainly packages which are impacted, for usability reasons. |
Not much progress here for a while. However, I'll mention one technique that could be helpful. It is safe in the sense that the otherwise unconstrained type variable will get the value which is requested in this issue, but it is inconvenient because the type arguments generally have to be specified explicitly. I think the difficulty that gave rise to this issue in the first place can be described by the following example: class Bloc<TEvent, TState> {}
class BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> {
BlocBuilder();
BlocBuilder.fromTBloc(TBloc bloc) {
print('BlocBuilder<$TBloc, $TState>');
}
}
class TestBloc extends Bloc<String, String> {}
class TestBlocBuilder extends BlocBuilder<TestBloc, String> {}
void main() {
// 2nd type argument just needs to be a supertype of `String`.
BlocBuilder<TestBloc, String>(); // OK.
BlocBuilder<TestBloc, Object>(); // OK.
BlocBuilder<TestBloc, dynamic>(); // OK.
// .. but we want `String`, to preserve the information.
// Inference from `TBloc` yields `dynamic` for `TState`.
BlocBuilder.fromTBloc(TestBloc()); // 'BlocBuilder<TestBloc, dynamic>'.
// .. which is the worst possible choice.
} The point is that However, we can tie // Using statically checked invariance, needs
// `--enable-experiment=variance`.
class Bloc<TEvent, inout TState> {}
class BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> {
BlocBuilder();
BlocBuilder.fromTBloc(TBloc bloc) {
print('BlocBuilder<$TBloc, $TState>');
}
}
class TestBloc extends Bloc<String, String> {}
class TestBlocBuilder extends BlocBuilder<TestBloc, String> {}
void main() {
// 2nd type argument must match the type argument of `TestBloc`.
BlocBuilder<TestBloc, String>(); // OK.
BlocBuilder<TestBloc, Object>(); // Compile-time error.
BlocBuilder<TestBloc, dynamic>(); // Compile-time error.
// Inference from `TBloc` fails.
BlocBuilder.fromTBloc(TestBloc()); // Compile-time error.
// We can specify the type argument manually,
// and only `String` is allowed with `TestBloc`.
BlocBuilder<TestBloc, String>.fromTBloc(TestBloc());
} We can emulate invariance in the current language: typedef Inv<X> = X Function(X);
class _Bloc<TEvent, TState, Invariance extends Inv<TState>> {}
typedef Bloc<TEvent, TState> = _Bloc<TEvent, TState, Inv<TState>>;
// ... remaining code is unchanged. Of course, this technique can only be used in the case where |
tl;dr A couple of solutions, if you're willing to adjust your code slightly and we get #3009 rd;lt Note that there is ongoing work in the area of type inference as mentioned in #3009 (comment). With that generalization we'd have the following: // When the type inference improvement is enabled.
class Bloc<TEvent, TState> {}
class BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> {
BlocBuilder();
BlocBuilder.fromTBloc(TBloc bloc) {
print('BlocBuilder<$TBloc, $TState>');
}
}
class TestBloc extends Bloc<String, String> {}
class TestBlocBuilder extends BlocBuilder<TestBloc, String> {}
void main() {
// Inference from `TBloc` yields `String` for `TState`.
BlocBuilder.fromTBloc(TestBloc()); // Prints 'BlocBuilder<TestBloc, String>'.
// .. which is what we want.
} This will handle the expression inference case as desired, which is the case that I was focusing on here. However, it does not provide a solution for the situation described in the first posting of this issue because that was a case where inference doesn't occur at all (so it doesn't help us that inference is improved). However, we can still solve the task in the original posting if we change the type hierarchy slightly, namely by changing one of the classes to be a mixin: abstract class FixBloc<TBloc> {}
class Bloc<TEvent, TState> {}
mixin BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> on FixBloc<TBloc> {}
class TestBloc extends Bloc<String, String> {}
class TestBlocBuilder extends FixBloc<TestBloc> with BlocBuilder {} In this example, the superclass The idea is that we're communicating to the mixin that it must have a particular actual type argument |
cross reference : felangel/bloc#560
currently in dart, we have to explicitly specify a type parameter for generic types, even when they can be inferred.
e.g. this is a valid class definition
but this isn't
you get an error at BlocBuilder definition:
I am not sure if this is an intentional design choice, or a bug that no one noticed, but i sure hope this gets fixed.
The text was updated successfully, but these errors were encountered: