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

Only partial passing of generic types #3119

Closed
escamoteur opened this issue May 31, 2023 · 2 comments
Closed

Only partial passing of generic types #3119

escamoteur opened this issue May 31, 2023 · 2 comments
Labels
request Requests to resolve a particular developer problem

Comments

@escamoteur
Copy link

OK, I had problems how to phrase this issue correctly. I'm currently working on watchIt the successor for the get_it_mixin and hit again a limitation of current Dart with generic types. In this case the function needs two generic types but one of them can be guessed by the analyzer without problems but I still have to pass both.

If we take this function here and I want to observe a propety name of a ChangeNotifier called Model.

R watchProperty<T extends Listenable, R>(R Function(T) selectProperty,
    {T? target, String? instanceName})

no I have to write

final name = watchProperty<Model,String>((x)=>name);

however it is possible to write

final name = watchProperty((Model x)=>name);

In that case both generic types can be guessed correctly by the analyser. But it feels like a bit of a workaround because I want to pass the first type to tell the function which type it should access and then deduct from the selector function the type of the other parameter. So that you could type

final name = watchProperty<Model>((x)=>name);

not really clear how that could be solved syntactically perhaps like:

final name = watchProperty<Model,>((x)=>name);
//or
final name = watchProperty<Model,_>((x)=>name);
//or
final name = watchProperty<Model,?>((x)=>name);
@escamoteur escamoteur added the request Requests to resolve a particular developer problem label May 31, 2023
@eernstg
Copy link
Member

eernstg commented May 31, 2023

With this particular example I'd actually argue that watchProperty((Model x) => name) is a very good fit (where name is some expression of type String): It specifies explicitly that the callback expects to receive an argument of type Model, which is useful to know when the code in the body of the callback is being written, maintained, or read. It also helps directly during the analysis of the callback that yields a return type, and that in turn provides information to the overall invocation of watchProperty, yielding the desired actual type arguments by inference.

In short, you're specifying the typing information at a crucial point in the code (where it's actually helping a reader of the code). This typing information then flows in useful ways to all other parts of the invocation.

So maybe the fix is to stop considering that approach as a workaround? ;-)

There is a more general issue here, though: We might want to use a type parameter which has a non-trivial upper bound, because this allows us to preserve information. In the following example, we use Iter extends Iterable<Elm> in order to preserve the information that the function returns a list if it is called with a list, and a set if it is called with a set, and so on.

Iter foo<Iter extends Iterable<Elm>, Elm>(Iter iter, Elm elm) {
  return switch (iter) {
    List<Elm>() => iter..add(elm),
    _ => iter,
  };
}

void main() {
  var xs = [1];
  var ys = foo(xs, 2); // `ys` has type `List<int>`, not just `Iterable<int>`.
}

We have considered using type patterns (#170) to provide a feature which is intended to do this kind of work. It basically relies on the ability to introduce type parameters by a pattern matching process rather than receiving them as actual type arguments from the call site. We would then do this:

Iter foo<Iter extends Iterable<final Elm>>(Iter iter, Elm elm) {
  return switch (iter) {
    List<Elm>() => iter..add(elm),
    _ => iter,
  };
}

void main() {
  var xs = foo([1.5], 2); // `xs` has type `List<double>`, `2` means `2.0`.
}

In main, we would infer Iter to be List<double> , which causes Elm to get the value double, which in turn causes 2 to be a value of type double (also written as 2.0). If you want to pass the type argument explicitly then you'd simply pass List<double> (just one type argument), and Elm is still computed based on the fact that the type argument of List<double> at Iterable is double.

@escamoteur
Copy link
Author

thanks a lot @eernstg you are right, I never had a problem with this but it seems users of my package seem to struggle if they don't pass the type as a generic parameter.

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

2 participants