-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Generic type in not inferred when used outside of class that is generalized #52249
Comments
You don't pass any final processedList = await process(delegate, list); is the same as final processedList = await process<Base>(delegate, list); This means you get |
@mraleph Is it intended that type is not inferred from the parameter ( Is there a way in Dart to infer type from the parameter, or is it possible to write smth like: for<T> (final EntityDelegate<T> delegate in delegates) {
final processedList = await process<T>(delegate, list);
print("Processed: $processedList");
} |
@MichaelDark There is no type erasure here - it's important to understand that inference works on static types, not on runtime types. The static type of Currently the only way to extract type argument runtime type of the object is to access it within a scope of a method, e.g. you could write something like class EntityDelegate<T extends Base> {
R extractT<R>(R Function<X extends Base>(EntityDelegate<X>) body) => body<T>(this);
}
// Note: Process<T extends Base> defines a generic typedef, what you need
// is typedef of a generic function.
typedef ProcessCallback = Future<List<T>> Function<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
);
Future<void> testProcessing(ProcessCallback process) async {
final List<dynamic> list = ["a", "b", "c"];
for (final delegate in delegates) {
final processedList = await delegate.extractT(
<T extends Base>(d) => process(d, list));
print("Processed: $processedList");
}
} This is rather cumbersome but today this is the only way to take concrete type arguments and use them for something. In the early days of Dart 3 design patterns proposal contained type parameter patterns, which would be a much more concise way to do the same: for (final EntityDelegate<var T> delegate in delegates) {
final processedList = await process<T>(delegate, list);
print("Processed: $processedList");
} But it this feature was removed from the proposal because it had an enormous implementation complexity and we were not ready for this. |
@mraleph Thank you for the comprehensive answer! At least now I know the way how to implement it. Tricky, but working. Type Parameter Patterns refers to dart-lang/language#170, correct? |
If you do not mind, I will refer to this type extraction as "magic". Full code: void main() async {
+ print('MAGIC, that worked anyway');
+ await testProcessingWithMagic(worksFine);
+ print('');
+
+ print('MAGIC, that failed before');
+ await testProcessingWithMagic(throwsTypeError);
+ print('');
+
print('code below works');
await testProcessing(worksFine);
print('');
print('code below fails. why? 0_o');
await testProcessing(throwsTypeError);
print('');
}
// Entities
abstract class Base {}
class EntityA extends Base {
final String valueA;
EntityA(this.valueA);
factory EntityA.fromValue(dynamic value) => EntityA(value);
@override
String toString() => "EntityA($valueA)";
}
class EntityB extends Base {
final String valueB;
EntityB(this.valueB);
factory EntityB.fromValue(dynamic value) => EntityB(value);
@override
String toString() => "EntityB($valueB)";
}
abstract class Dao<T> {
Future<void> insert(List<T> list);
}
class BaseDao<T extends Base> extends Dao<T> {
BaseDao();
@override
Future<void> insert(List<T> list) async {
print("inserting [$T]: $list");
}
}
class EntityDelegate<T extends Base> {
final T Function(dynamic) fromValue;
final Dao<T> Function() createDao;
EntityDelegate(this.fromValue, this.createDao);
Future<List<T>> convertAndSave(List<dynamic> list) async {
final List<T> convertedList = list.map(fromValue).toList();
final Dao<T> dao = createDao();
await dao.insert(convertedList);
return convertedList;
}
+
+ // MAXIMUM MAGIC
+ R extractT<R>(R Function<X extends Base>(EntityDelegate<X>) body) =>
+ body<T>(this);
}
// Magic below
final List<EntityDelegate> delegates = [
EntityDelegate<EntityA>(EntityA.fromValue, () => BaseDao<EntityA>()),
EntityDelegate<EntityB>(EntityB.fromValue, () => BaseDao<EntityB>()),
];
typedef ProcessCallback<T extends Base> = Future<List<T>> Function(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
);
Future<void> testProcessing(ProcessCallback process) async {
final List<dynamic> list = ["a", "b", "c"];
for (final delegate in delegates) {
final processedList = await process(delegate, list);
print("Processed: $processedList");
}
}
Future<List<T>> worksFine<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
) async {
final convertedList = await delegate.convertAndSave(sourceList);
print("Converted: $convertedList");
return convertedList;
}
Future<List<T>> throwsTypeError<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
) async {
final List<T> convertedList = sourceList.map(delegate.fromValue).toList();
final Dao<T> dao = delegate.createDao();
await dao.insert(convertedList); // fails here with:
// TypeError: Instance of 'JSArray<Base>': type 'JSArray<Base>' is not a subtype of type 'List<EntityA>
// or
// TypeError: Instance of 'List<Base>': type 'List<Base>' is not a subtype of type 'List<EntityA>
print("Converted: $convertedList");
return convertedList;
}
+// MAXIMUM MAGIC BELOW
+
+// Note: Process<T extends Base> defines a generic typedef, what you need
+// is typedef of a generic function.
+typedef ProcessCallbackWithMagic = Future<List<T>> Function<T extends Base>(
+ EntityDelegate<T> delegate,
+ List<dynamic> sourceList,
+);
+
+Future<void> testProcessingWithMagic(ProcessCallbackWithMagic process) async {
+ final List<dynamic> list = ["a", "b", "c"];
+
+ for (final delegate in delegates) {
+ final processedList =
+ await delegate.extractT(<T extends Base>(d) => process(d, list));
+ print("Processed: $processedList");
+ }
+}
+ |
I'm still somewhat unhappy about the situation here. It would be nice to be able to characterize the situation in an actionable manner: How do we detect that there is a potential for dynamic type errors? The situation doesn't immediately match the well-known scenarios where this kind of issue comes up. One thing to notice is that the approach where It actually still encounters a dynamic type error if we allow type inference to choose the type arguments when the entity delegates are created (it chooses the value Example program, throws with magicvoid main() async {
print('MAGIC, that worked anyway');
await testProcessingWithMagic(worksFine);
print('');
print('MAGIC, that failed before');
await testProcessingWithMagic(throwsTypeError);
print('');
}
// Entities
abstract class Base {}
class EntityA extends Base {
final String valueA;
EntityA(this.valueA);
factory EntityA.fromValue(dynamic value) => EntityA(value);
@override
String toString() => "EntityA($valueA)";
}
class EntityB extends Base {
final String valueB;
EntityB(this.valueB);
factory EntityB.fromValue(dynamic value) => EntityB(value);
@override
String toString() => "EntityB($valueB)";
}
abstract class Dao<T> {
Future<void> insert(List<T> list);
}
class BaseDao<T extends Base> extends Dao<T> {
BaseDao();
@override
Future<void> insert(List<T> list) async {
print("inserting [$T]: $list");
}
}
class EntityDelegate<T extends Base> {
final T Function(dynamic) fromValue;
final Dao<T> Function() createDao;
EntityDelegate(this.fromValue, this.createDao);
Future<List<T>> convertAndSave(List<dynamic> list) async {
final List<T> convertedList = list.map(fromValue).toList();
final Dao<T> dao = createDao();
await dao.insert(convertedList);
return convertedList;
}
// MAXIMUM MAGIC
R extractT<R>(R Function<X extends Base>(EntityDelegate<X>) body) =>
body<T>(this);
}
// Magic below
final List<EntityDelegate<Base>> delegates = [
EntityDelegate(EntityA.fromValue, BaseDao<EntityA>.new),
EntityDelegate(EntityB.fromValue, BaseDao<EntityB>.new),
];
typedef ProcessCallback<T extends Base> = Future<List<T>> Function(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
);
Future<List<T>> worksFine<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
) async {
final convertedList = await delegate.convertAndSave(sourceList);
print("Converted: $convertedList");
return convertedList;
}
Future<List<T>> throwsTypeError<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
) async {
final List<T> convertedList = sourceList.map(delegate.fromValue).toList();
final Dao<T> dao = delegate.createDao();
await dao.insert(convertedList); // fails here with `TypeError`.
print("Converted: $convertedList");
return convertedList;
}
// MAXIMUM MAGIC BELOW
typedef ProcessCallbackWithMagic = Future<List<T>> Function<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
);
Future<void> testProcessingWithMagic(ProcessCallbackWithMagic process) async {
final List<dynamic> list = ["a", "b", "c"];
for (final delegate in delegates) {
final processedList = await delegate.extractT<Future<List<Object?>>>(
<T extends Base>(d) => process(d, list));
print("Processed: $processedList");
}
} This program compiles and runs (for instance, in DartPad), but it soon encounters a dynamic type error (and if we comment out the first invocation of The reason why this happens is that we're treating a So we could try to change Example program, makes Dao and EntityDelegate invariantvoid main() async {
print('MAGIC, that worked anyway');
await testProcessingWithMagic(worksFine);
print('');
print('MAGIC, that failed before');
await testProcessingWithMagic(throwsTypeError);
print('');
}
// Entities
abstract class Base {}
class EntityA extends Base {
final String valueA;
EntityA(this.valueA);
factory EntityA.fromValue(dynamic value) => EntityA(value);
@override
String toString() => "EntityA($valueA)";
}
class EntityB extends Base {
final String valueB;
EntityB(this.valueB);
factory EntityB.fromValue(dynamic value) => EntityB(value);
@override
String toString() => "EntityB($valueB)";
}
// Magic below
final List<EntityDelegateBase> delegates = [
EntityDelegate<EntityA>(EntityA.fromValue, BaseDao<EntityA>.new),
EntityDelegate<EntityB>(EntityB.fromValue, BaseDao<EntityB>.new),
];
typedef ProcessCallback<T extends Base> = Future<List<T>> Function(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
);
Future<List<T>> worksFine<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
) async {
final convertedList = await delegate.convertAndSave(sourceList);
print("Converted: $convertedList");
return convertedList;
}
Future<List<T>> throwsTypeError<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
) async {
final List<T> convertedList = sourceList.map(delegate.fromValue).toList();
final Dao<T> dao = delegate.createDao();
await dao.insert(convertedList); // fails here with `TypeError`.
print("Converted: $convertedList");
return convertedList;
}
// MAXIMUM MAGIC BELOW
typedef ProcessCallbackWithMagic = Future<List<T>> Function<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
);
Future<void> testProcessingWithMagic(ProcessCallbackWithMagic process) async {
final List<dynamic> list = ["a", "b", "c"];
for (final delegate in delegates) {
final processedList = await delegate.extractT<Future<List<Object?>>>(
<T extends Base>(d) => process<T>(d, list));
print("Processed: $processedList");
}
}
// ----------------------------------------------------------------------
// Emulating invariance.
/* If we had a built-in mechanism we could just mark the type variables:
abstract class Dao<inout T> {
Future<void> insert(List<T> list);
}
class BaseDao<inout T extends Base> extends Dao<T> {
BaseDao();
@override
Future<void> insert(List<T> list) async {
print("inserting [$T]: $list");
}
}
*/
typedef Dao<T> = _Dao<T, _Inv<T>>;
typedef BaseDao<T extends Base> = _BaseDao<T, _Inv<T>>;
typedef _Inv<T> = T Function(T);
abstract class _Dao<T, Invariance> {
Future<void> insert(List<T> list);
}
class _BaseDao<T extends Base, Invariance> extends _Dao<T, Invariance> {
_BaseDao();
@override
Future<void> insert(List<T> list) async {
print("inserting [$T]: $list");
}
}
abstract class EntityDelegateBase {
R extractT<R>(R Function<X extends Base>(EntityDelegate<X>) body);
}
typedef EntityDelegate<T extends Base> = _EntityDelegate<T, _Inv<T>>;
class _EntityDelegate<T extends Base, Invariance>
implements EntityDelegateBase {
final T Function(dynamic) fromValue;
final Dao<T> Function() createDao;
_EntityDelegate(this.fromValue, this.createDao);
Future<List<T>> convertAndSave(List<dynamic> list) async {
final List<T> convertedList = list.map(fromValue).toList();
final Dao<T> dao = createDao();
await dao.insert(convertedList);
return convertedList;
}
R extractT<R>(R Function<X extends Base>(EntityDelegate<X>) body) =>
body<T>(this as EntityDelegate<T>);
} I've done several things here: Next, I've changed the declaration of
The expressions where the entity delegates are created have been modified: I've re-added the actual type arguments (because type inference can't compute them so there's a compile-time error if we leave them out). The crucial point is that it is now a compile-time error to use the actual type argument Now where Finally, I've added an explicit type argument to the invocation of But the point is that all we need to do is to make We might think that we can now drop the It throws at run time, showing that we do need Example program, re-visiting `testProcessing`void main() async {
print('code below works');
await testProcessing(worksFine);
print('');
print('code below fails. why? 0_o');
await testProcessing(throwsTypeError);
print('');
}
// Entities
abstract class Base {}
class EntityA extends Base {
final String valueA;
EntityA(this.valueA);
factory EntityA.fromValue(dynamic value) => EntityA(value);
@override
String toString() => "EntityA($valueA)";
}
class EntityB extends Base {
final String valueB;
EntityB(this.valueB);
factory EntityB.fromValue(dynamic value) => EntityB(value);
@override
String toString() => "EntityB($valueB)";
}
final List<EntityDelegateBase> delegates = [
EntityDelegate<EntityA>(EntityA.fromValue, BaseDao<EntityA>.new),
EntityDelegate<EntityB>(EntityB.fromValue, BaseDao<EntityB>.new),
];
typedef ProcessCallback<T extends Base> = Future<List<T>> Function(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
);
Future<void> testProcessing(ProcessCallback<Base> process) async {
final List<dynamic> list = ["a", "b", "c"];
for (final delegate in delegates) {
// At this point we can't find the correct type argument to pass to `process`.
// In one iteration it should be `EntityA`, in the next iteration it should be
// `EntityB`, and it will not work to pass `Base` in both cases. So we're in
// deep trouble and the only real fix is to add in `extractT` again.
final processedList = await process(delegate, list);
print("Processed: $processedList");
}
}
Future<List<T>> worksFine<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
) async {
final convertedList = await delegate.convertAndSave(sourceList);
print("Converted: $convertedList");
return convertedList;
}
Future<List<T>> throwsTypeError<T extends Base>(
EntityDelegate<T> delegate,
List<dynamic> sourceList,
) async {
final List<T> convertedList = sourceList.map(delegate.fromValue).toList();
final Dao<T> dao = delegate.createDao();
await dao.insert(convertedList); // fails here with `TypeError`.
print("Converted: $convertedList");
return convertedList;
}
// ----------------------------------------------------------------------
// Dao, modified to be invariant.
/* If we had a built-in mechanism we could just mark the type variables:
abstract class Dao<inout T> {
Future<void> insert(List<T> list);
}
class BaseDao<inout T extends Base> extends Dao<T> {
BaseDao();
@override
Future<void> insert(List<T> list) async {
print("inserting [$T]: $list");
}
}
*/
typedef Dao<T> = _Dao<T, _Inv<T>>;
typedef BaseDao<T extends Base> = _BaseDao<T, _Inv<T>>;
typedef _Inv<T> = T Function(T);
abstract class _Dao<T, Invariance> {
Future<void> insert(List<T> list);
}
class _BaseDao<T extends Base, Invariance> extends _Dao<T, Invariance> {
_BaseDao();
@override
Future<void> insert(List<T> list) async {
print("inserting [$T]: $list");
}
}
// EntityDelegate, modified to be invariant.
abstract class EntityDelegateBase {}
typedef EntityDelegate<T extends Base> = _EntityDelegate<T, _Inv<T>>;
class _EntityDelegate<T extends Base, Invariance>
implements EntityDelegateBase {
final T Function(dynamic) fromValue;
final Dao<T> Function() createDao;
_EntityDelegate(this.fromValue, this.createDao);
Future<List<T>> convertAndSave(List<dynamic> list) async {
final List<T> convertedList = list.map(fromValue).toList();
final Dao<T> dao = createDao();
await dao.insert(convertedList);
return convertedList;
}
} But then we can't restore the information that each entity delegate is an So we really do need the OK, sorry about the large amount of code rambling here, but I really wanted to understand how this issue could come up, and I think it's much clearer now. For me, at least. ;-) |
@stereotype441, if you're around, do you have an idea about why we have to specify the actual type argument Edit: Created #52850 where this behavior is singled out in a simpler example. |
Most likely, that this issue is related to dart-lang/language#524 (Sound declaration-site variance).
Tested in DartPad (Dart SDK 2.19.6).
Brief:
Case 1: generic processing is encapsulated in the delegate.
Expected: compiles, runs without errors
Actual: compiles, runs without errors
Case 2: generic processing is not in the delegate class, delegate's fields are used directly.
Expected: compiles, runs without errors
Actual: compiles, runs with runtime error
Compiles, but throws at runtime:
TypeError: Instance of 'JSArray<Base>': type 'JSArray<Base>' is not a subtype of type 'List<EntityA>
In general, questions are:
Full code snippet:
Related errors:
TypeError: Instance of 'List': type 'List' is not a subtype of type 'List
TypeError: Instance of 'JSArray': type 'JSArray' is not a subtype of type 'List
The text was updated successfully, but these errors were encountered: