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

Extensions on nested generic mixins don't promote types properly #3382

Open
ahmednfwela opened this issue Oct 2, 2023 · 6 comments
Open
Labels
request Requests to resolve a particular developer problem

Comments

@ahmednfwela
Copy link

ahmednfwela commented Oct 2, 2023

When declaring extensions on nested generic mixins Mixin<Mixin<T>> for example; the last type T is lost, and the compiler thinks it's dynamic.
leading to runtime errors when the type mismatches.

mixin TestMixin<T> {
  void operateOnValue(T value);
}
// First class expects a string.
class C1 with TestMixin<String> {
  @override
  void operateOnValue(String value) {
    print('operateOnValue: $value');
  }
}

// Second class expects C1 which is a TestMixin<String>
// so this class is matched with TestMixin<TestMixin<String>>
class C2 with TestMixin<C1> {
  @override
  void operateOnValue(C1 value) {
    // do nothing.
  }
}

// extension targets TestMixin<TestMixin<TSub>>.
extension NestedMixinExt<TSub, TestSub extends TestMixin<TSub>>
    on TestMixin<TestSub> { 
  void operateOnNestedValue(TestSub nestedMixin, TSub sub) {
    print('calling operateOnValue, sub type is: $TSub');
    nestedMixin.operateOnValue(sub);
  }
}

void main(List<String> arguments) {
  final c1 = C1(); //TestMixin<String>
  final c2 = C2(); //TestMixin<C1> == TestMixin<TestMixin<String>>

  c2.operateOnNestedValue(c1, 'hello'); // prints "operateOnValue: hello" as expected.
  c2.operateOnNestedValue(c1, 50); // compiles normally, and throws runtime error.
  // type 'int' is not a subtype of type 'String' of 'value'
}

output of the program is:

calling operateOnValue with value hello, sub type is: dynamic
operateOnValue: hello
calling operateOnValue with value 50, sub type is: dynamic
Unhandled exception:
type 'int' is not a subtype of type 'String' of 'value'

also in my use case, I can't change the extension to

extension NestedMixinExt<TSub> on TestMixin<TestMixin<TSub>>

since I need access to the exact nested type.

dart info:

  • Dart 3.1.3 (stable) (Tue Sep 26 14:25:13 2023 +0000) on "windows_x64"
  • on windows / "Windows 10 Pro N" 10.0 (Build 19045)
@lrhn
Copy link
Member

lrhn commented Oct 2, 2023

extension NestedMixinExt<TSub, TestSub extends TestMixin<TSub>>
    on TestMixin<TestSub> { 

this looks like generic parameter which are under-constrained in a match.
It's possible to choose Object? for TSub, and it will still match the same type.

Try having just one type parameter:

extension NestedMixinExt<TSub> on TestMixin<TestMixin<TSub>> { 
  void operateOnNestedValue(TestMixin<TSub> nestedMixin, TSub sub) {
    print('calling operateOnValue, sub type is: $TSub');
    nestedMixin.operateOnValue(sub);
  }
}

Generally, if you have something with two type parameters, trying to infer them from something that only has one, is very often going to leave one of the type parameters under-constrained, and you risk it being inferred as Object? or similar.

@ahmednfwela
Copy link
Author

@lrhn Thanks for your response.

However, as stated in the issue:

also in my use case, I can't change the extension to

extension NestedMixinExt<TSub> on TestMixin<TestMixin<TSub>>

since I need access to the exact nested type.

this is because the function signature I have is actually something like this:

TestSub operateOnNestedValue(TestSub nestedMixin, TSub sub)

where I need to return the exact input type.

@eernstg
Copy link
Member

eernstg commented Oct 3, 2023

Note that a very similar issue has been discussed in #620. The common ground is that both this issue and 620 asks for the ability to receive a type argument like TestSub, find the superinterface of the actual value of TestSub which is of the form TestMixin<Tsub> for some type Tsub, and use that to infer the value of Tsub.

@lrhn
Copy link
Member

lrhn commented Oct 3, 2023

The real constraint that is desired here is that for

extension NestedMixinExt<TSub, TestSub extends TestMixin<TSub>>

the second occurrence of TSub is invariant.

That way, matching a TestMixin<int> can only succeed by binding TSub to int.
And not, like today, allow TSub to be Object and TestSub to extend TestMixin<Object>.
That's correct, but not useful.

If we get variance annotations, maybe it's possible to make TestMixin invariant. That solves this problem.
Maybe it's not, then we'd want something like

extension NestedMixinExt<TSub, TestSub extends TestMixin<inout TSub>>

which forces an invariant subtype check on TestMixin.

Still, it's much more complicated to introduce further constraints, either in subtyping or between type parameters, than just saying that you want "the type that TestMixin matched at":

extension NestedMixinExt<TestSub extends TestMixin<final TSub>>

That's the most direct expression of what is really wanted: Not a second parameter that is forced to relate to the other parameter's type, but simply the actual type parameter that was found for that other parameter.

Also avoids having to ignore some type parameters when calling, because they're not useful, they're just there to attempt to capture types on the side (which just doesn't work today).

@ahmednfwela
Copy link
Author

extension NestedMixinExt<TestSub extends TestMixin<final TSub>>

exactly! this is what I hope would be possible, just like pattern matching with instances, I hope there will be pattern matching with types.

@lrhn lrhn transferred this issue from dart-lang/sdk Oct 6, 2023
@lrhn lrhn added the request Requests to resolve a particular developer problem label Oct 6, 2023
@eernstg
Copy link
Member

eernstg commented Oct 9, 2023

We should note that it is already possible to handle the given example in a very direct manner:

mixin TestMixin<T> {
  void operateOnValue(T value);
}

// First class expects a string.
class C1 with TestMixin<String> {
  @override
  void operateOnValue(String value) {
    print('operateOnValue: $value');
  }
}

// Second class expects C1 which is a TestMixin<String>
// so this class is matched with TestMixin<TestMixin<String>>
class C2 with TestMixin<C1> {
  @override
  void operateOnValue(C1 value) {
    // do nothing.
  }
}

// extension targets TestMixin<TestMixin<TSub>>.
extension NestedMixinExt<TSub> on TestMixin<TestMixin<TSub>> {
  void operateOnNestedValue(TestMixin<TSub> nestedMixin, TSub sub) {
    print('calling operateOnValue, sub type is: $TSub');
    nestedMixin.operateOnValue(sub);
  }
}

void main(List<String> arguments) {
  final c1 = C1(); // TestMixin<String>
  final c2 = C2(); // TestMixin<C1> == TestMixin<TestMixin<String>>

  c2.operateOnNestedValue(c1, 'hello'); // Prints "operateOnValue: hello" as expected.
  c2.operateOnNestedValue(c1, 50); // Compile-time error.
}

The idea is simply that we specify the extension such that it denotes the desired type directly. This causes TSub to be inferred as String when the receiver has the type C2, and that makes 50 a type error in the second invocation.

A proposal for a more general mechanism that would allow us to look up the value of a type argument in a superinterface is given here: #3324, but I think it's interesting that we can already do certain things in this area using extensions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

3 participants