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

Generic type in not inferred when used outside of class that is generalized #52249

Closed
MichaelDark opened this issue May 3, 2023 · 7 comments
Closed
Labels
closed-as-intended Closed as the reported issue is expected behavior type-question A question about expected behavior or functionality

Comments

@MichaelDark
Copy link

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

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;
  }
}
// ...
final convertedList = await delegate.convertAndSave(sourceList);

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>

final List<T> convertedList = sourceList.map(delegate.fromValue).toList();
final Dao<T> dao = delegate.createDao();
await dao.insert(convertedList); // fails here

In general, questions are:

  • is it a bug or intended behavior?
  • If bug - which issue to address?
  • If intended behavior - what is the explanation of why type is not inferred?

Full code snippet:

void main() async {
  print('code below works');
  await testProcessing(worksFine);

  print('code below fails. why? 0_o');
  await testProcessing(throwsTypeError);
}

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

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

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

@mraleph
Copy link
Member

mraleph commented May 3, 2023

You don't pass any T into throwsTypeError so it naturally ends up being base Base. In other words:

    final processedList = await process(delegate, list);

is the same as

    final processedList = await process<Base>(delegate, list);

This means you get convertedList of type List<Base>. However the delegate itself is carrying more precise type so you get type error. The difference from your first case is that T is reified on the receiver.

@mraleph mraleph closed this as completed May 3, 2023
@mraleph mraleph added closed-as-intended Closed as the reported issue is expected behavior type-question A question about expected behavior or functionality labels May 3, 2023
@MichaelDark
Copy link
Author

@mraleph Is it intended that type is not inferred from the parameter (delegate)? Delegate for sure have the type parameter (for example, EntityDelegate<EntityA>), but when passed as an argument - type erasure is lost in the scope of process method.

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

@mraleph
Copy link
Member

mraleph commented May 4, 2023

@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 delegates is List<EntityDelegate> (which is the same as List<EntityDelegate<Base>> according to instantiate to bounds rules). This means delegate has static type EntityDelegate<Base> which does not provide any additional information for inferring something more precise than Base as a type argument for process.

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.

@MichaelDark
Copy link
Author

@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?

@MichaelDark
Copy link
Author

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");
+  }
+}
+

@eernstg
Copy link
Member

eernstg commented Jul 4, 2023

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 extractT is used to provide access to the actual type argument of the EntityDelegate will help as long as each entity delegate is created in a disciplined way.

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 Base). So I modified the declaration of delegates slightly in order to show what happens in this case.

Example program, throws with magic
void 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 testProcessingWithMagic then we can see that the second one throws as well).

The reason why this happens is that we're treating a Dao as if it were covariant in its type argument, even though it is actually contravariant (because T is used just once in the body of Dao and BaseDao, and that is as a parameter type, which is a contravariant position).

So we could try to change Dao to be invariant in its type argument. Given that we don't have a full implementation of #524, sound declaration-site variance, we can't use a built-in language mechanism, but we can still emulate invariance. So here we go:

Example program, makes Dao and EntityDelegate invariant
void 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: Dao and BaseDao have been modified to be invariant in their type argument (and there's a comment showing how much nicer that would look with #524 ;-). EntityDelegates has been changed similarly.

Next, I've changed the declaration of delegates and the invocation of process:

71,72c58,59
<   EntityDelegate(EntityA.fromValue, BaseDao<EntityA>.new),
<   EntityDelegate(EntityB.fromValue, BaseDao<EntityB>.new),
---
>   EntityDelegate<EntityA>(EntityA.fromValue, BaseDao<EntityA>.new),
>   EntityDelegate<EntityB>(EntityB.fromValue, BaseDao<EntityB>.new),
113c100
<         <T extends Base>(d) => process(d, list));
---
>         <T extends Base>(d) => process<T>(d, list));

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 Base, because then the BaseDao constructor isn't a type correct argument to the constructor any more. So we've eliminated the danger that the entity delegate could otherwise have a too-general actual type argument Base (which was the reason for the dynamic type errors in the previous example).

Now where EntityDelegate is invariant in the type argument, we can't have a list of EntityDelegate<Base> containing an EntityDelegate<EntityA> or B, because those types are simply unrelated now. So we need to introduce a new superclass EntityDelegateBase which doesn't have a type argument, and then delegates has been changed to be a list of EntityDelegateBase. Note that EntityDelegateBase has an abstract extractT, such that we can still get access to the actual type argument of the subtypes where it exists.

Finally, I've added an explicit type argument to the invocation of process. This makes the program succeed. Apparently, type inference can't provide that type argument to process --- that may or may not be a type inference bug.

But the point is that all we need to do is to make Dao, BaseDao, and EntityDelegate invariant in their type argument, and then the typing changes here and there, and the result is a type safe program.

We might think that we can now drop the extractT magic again, because invariant types "do not forget their type argument", and we just made various types invariant. However, that is not true. The following example doesn't have extractT, and we're simply calling various functions with the "naive" values of type arguments.

It throws at run time, showing that we do need extractT.

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 EntityDelegate<T> for any particular T.

So we really do need the extractT method in order to restore that type parameter.

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

@eernstg
Copy link
Member

eernstg commented Jul 4, 2023

@stereotype441, if you're around, do you have an idea about why we have to specify the actual type argument T explicitly in the invocation of process<T>(d, list), and whether it is working as intended?

Edit: Created #52850 where this behavior is singled out in a simpler example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-as-intended Closed as the reported issue is expected behavior type-question A question about expected behavior or functionality
Projects
None yet
Development

No branches or pull requests

3 participants