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

Class generic type not resolved for function field using same generics without specification #48108

Closed
idraper opened this issue Jan 8, 2022 · 3 comments

Comments

@idraper
Copy link

idraper commented Jan 8, 2022

dart version: Dart SDK version: 2.15.1 (stable)
os: windows 10

I have a visitor class that accepts a matcher function as a parameter to enable custom comparison when searching for an object in a tree. Sometimes it is required to return a stricter type than the one it is searching for (but extends from it), and so I have two generics, one for the find type (F) and the other is the return type (R).

The issue is best seen with the following example:

void main(List<String> arguments) {
  FindVisitor(0, (a, b) => a.abs() == b.abs());               // 1: error (not expected)
  FindVisitor<int, int>(0, (a, b) => a.abs() == b.abs());     // 2: no error (as expected)
  FindVisitor(0, (int a, int b) => a.abs() == b.abs());       // 3: no error (as expected)
  FindVisitor(0, (int a, Function b) => a.abs() == b.abs());  // 4: no error with function argument `b` - there should be
}

class FindVisitor<F, R extends F> {
  FindVisitor(this.find, this.matcher);

  final F find;
  final bool Function(F a, R b) matcher;
}

When creating the visitor, I expected that the dart analyzer would be able to automatically detect the generic type for the matcher, but it is not. The curious thing is that if I hover over the definition I can see that the analyzer correctly detected the generics, but a and b each have the type Object? instead of int for #1. Thus, calling a.abs() (or b.abs()) is an error since it doesn't know it is of type int.

Then, after creating this small example I found #4, where the anonymous declaration (int a, Function b) doesn't throw an error saying that b cannot be of type Function.

Thus, I am wondering about the issues with #1 not finding the types (yet they are when hovering...) and #4 not recognizing that the function type is invalid.

@vsmenon
Copy link
Member

vsmenon commented Jan 10, 2022

I do see a static error on the fourth call (with Function), both from analysis and compilation. Are you sure you're not seeing that?

I suspect the first error is working as intended. @eernstg @lrhn ?

@eernstg
Copy link
Member

eernstg commented Jan 10, 2022

@idraper, you need sound variance (e.g., dart-lang/language#524) in order to be able to use an instance variable whose type has an occurrence of a type variable of the enclosing class in a contravariant position. See also dart-lang/language#297 for more information about the situation, and another potential language design take on it.

If you do have an instance variable with that property after all then it is potentially a dynamic error to even evaluate the corresponding getter (you don't even have to call the function matcher, just getting hold of it is an error):

void main(List<String> arguments) {
  FindVisitor<num, num> v = FindVisitor<int, int>(0, (a, int b) => true);
  v.matcher; // Throws!
}

So, in practice, you want a different design. Perhaps you can use this:

// Define each matcher by writing a subtype (`extends` or `implements`)
// and override `matcher`.
abstract class FindVisitor1<F, R extends F> {
  FindVisitor(this.find);

  final F find;
  bool matcher(F a, R b);
}

class FindVisitor2<F, M extends bool Function(F, F)> {
  FindVisitor2(this.find, this.matcher);

  final F find;
  final M matcher;
}

The former one relies on the treatment of methods with parameter types where a type variable occurs covariantly, and that treatment is well-known (e.g., it's used by List.add and lots of other widely known methods). It is still possible to get a dynamic error if you call it with an actual argument whose type is too general (just like (<int>[] as List<num>).add(1.5)), but you will not get a dynamic error just because you're accessing the member (myFindVisitor1.matcher will not throw).

The latter one relies on eliminating the "contravariant instance variable" by using a subtype requirement on the function type. This means that the matcher can accept arguments of type F and F, or T1 and T2 for any types T1 and T2 such that F <: T1 and F <: T2. So you could have a matcher that accepts, say, dynamic and F?.

For the treatment of the example as stated, I see an error with b.abs(), because b has type Function, and Function does not have a member named abs.

It is not an error to pass the function literal of type bool Function(int, Function) as the second argument, it just means that the inferred type arguments to the constructor invocation must be int and Never (because the latter is a subtype of int and also a subtype of Function). You can see this as follows:

void main(List<String> arguments) {
  FindVisitor(0, (int a, Function b) => true).matcher(1, 1); // Error: Can't pass an `int` to `Never`.
}

I don't see anything here which isn't working as intended (except that we really need sound variance soon! ;-).

@idraper
Copy link
Author

idraper commented Jan 10, 2022

Thank you for the detailed response and references! Closing issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants