dart-lang / language

Design of the Dart language
Other
2.66k stars 205 forks source link

Infer generic type parameters based on the declaring type #620

Open bigworld12 opened 5 years ago

bigworld12 commented 5 years ago

cross reference : https://github.com/felangel/bloc/issues/560

currently in dart, we have to explicitly specify a type parameter for generic types, even when they can be inferred. e.g. this is a valid class definition

class Bloc<TEvent,TState> {}  
class BlocBuilder<TBloc extends Bloc<dynamic,TState>,TState> {}

class TestBloc extends Bloc<String,String> {}
class TestBlocBuilder extends BlocBuilder<TestBloc,String> {}

but this isn't

class Bloc<TEvent,TState> {}  
class BlocBuilder<TBloc extends Bloc<dynamic,TState>> {}

class TestBloc extends Bloc<String,String> {}
class TestBlocBuilder extends BlocBuilder<TestBloc> {}

you get an error at BlocBuilder definition:

The name 'TState' isn't a type so it can't be used as a type argument.
Try correcting the name to an existing type, or defining a type named 'TState'.

I am not sure if this is an intentional design choice, or a bug that no one noticed, but i sure hope this gets fixed.

leafpetersen commented 5 years ago

I'm really not sure what you're asking for here. Are you suggesting that given an example like:

class BlocBuilder<TBloc extends Bloc<dynamic,TState>> {}

in the case that TState is not an already declared identifier in scope, we should treat otherwise unbound free variables as being implicitly part of the parameter list? What if there are multiple ones? How do we order them (and how does the user see what order was chosen)? In general, this seems like something that's going to be extremely fragile and surprising to users. Am I misunderstanding what you are asking?

bigworld12 commented 5 years ago

i think a simpler way to express this is to make the extends clause able to define type parameters, not only testing against them.

or another way of solving this is to keep

class BlocBuilder<TBloc extends Bloc<dynamic,TState>,TState> {}

as it is, but make this a valid definition by inferring TState

BlocBuilder<TestBloc>()
leafpetersen commented 5 years ago

Ok, leaving aside the question of implicitly defining type parameters which I think is problematic, the question of inferring missing type arguments seems more reasonable. I think you're proposing that if type arguments are left off (as in BlockBuilder<TestBloc>) that we solve for the missing type arguments based on the ones provided. This is probably more technically feasible. My initial reaction is that I'd want some kind of explicit syntax to indicate to the reader of the code that there were missing arguments there, e.g. BlockBuilder<TestBloc, _>, but perhaps there's a good argument for allowing missing trailing arguments implicitly since it could permit adding type parameters to be a non-breaking change in some situations.

Without explicit per variable syntax for the elided variables, we'd need to restrict this to trailing arguments. Otherwise you don't know which parameters to match up the arguments provided against.

bigworld12 commented 5 years ago

I am not sure having to explicitly state a place holder for inferred types is necessary, the compiler can just check if the type argument has been assigned before to infer it.

the checking part already happens though, e. g.

BlocBuilder<TestBloc, int> 

will give a compile-time error, since int is not of type String.

leafpetersen commented 5 years ago

I am not sure having to explicitly state a place holder for inferred types is necessary,

It is necessary if you want to allow arguments other than the trailing arguments to be omitted.

bigworld12 commented 5 years ago

do you mean this case ?

class BlocBuilder<TBloc extends Bloc<TEvent,TState>,TEvent,TState,TSomethingElse> {}

BlocBuilder<TestBloc,TestSomethingElse>()

this makes sense, since the compiler isn't sure whether you want to check TestSomethingElse for extending TEvent, or if it's an implicitly defined argument,

so i think we have to allow only trailing arguments to be omitted

bigworld12 commented 5 years ago

we can even add some modifier at the declaration to specify the type as implicit, this way we don't have to care where is it located, e.g. implicit modifier

class BlocBuilder<TBloc extends Bloc<dynamic,TState>, implicit TState>

making this a valid declaration :

class BlocBuilder<implicit TEvent, TBloc extends Bloc<TEvent,TState>,implicit TState,TSomethingElse> {}

BlocBuilder<TestBloc,TestSomethingElse>()
leafpetersen commented 5 years ago

Thinking about this some more, I wonder if this isn't actually more of a request for patterns rather than inference. That is, something more like (inventing some syntax):

class Bloc<TEvent,TState> {}  
// BlocBuilder only takes one type argument, but it pattern matches against that
// argument and binds TState to the second argument of TBloc.
class BlocBuilder<TBloc as Bloc<dynamic,Type TState>> {}

class TestBloc extends Bloc<String,String> {}
class TestBlocBuilder extends BlocBuilder<TestBloc> {}

cc @munificent I wonder if this is another pattern matching use case to consider? I do feel like I've seen this style of code before, where a type variable exists just to give a name to a part of another type variable.

bigworld12 commented 5 years ago

is there any update on this ? I think @leafpetersen 's proposal is pretty good

eernstg commented 5 years ago

We could use type patterns for this, cf. #170:

class BlocBuilder<TBloc extends Bloc<dynamic, var TState>> {}

This would then perform static type pattern matching only (there is no need for a match at run time, because the actual type arguments will be denotable types at instance creation, and the construct would be desugared to have two type arguments).

The type parameters that are introduced by matching would have bounds derived from the context of the pattern (so TState would have the bound required to allow it to be passed to Bloc). It is possible that this would give rise to some non-trivial equation solving tasks, so that's definitely one thing to look out for (and possibly use to reject some patterns because they're intractable).

There are two perspectives on this, and I think that both amount to a useful feature:

  1. It's just like <TBloc extends Bloc<dynamic, TState>, TState>, except that the parameterized types in client land can be more concise.
  2. It's a way for the body of BlocBuilder to decompose TBloc, which would be a useful feature anyway (and which could be provided as a new form of <type> using type patterns), but this particular approach might be extra convenient because it's so concise.

So this would actually be quite nice!

rrousselGit commented 4 years ago

For now, we can make a custom method to partially solve this issue:

abstact class Foo<Param> {
  R capture<R>(R cb<T>()) {
    return cb<Param>();
  }
}

Which can then be used this way:

void printGenericValue<T extends Foo<dynamic>>(T value) {
  print(value.capture(<Param>() => Param));
}

class Bar extends Foo<int> {}

void main() {
  printGenericValue(Bar()); // prints `int`
}

This is not ideal, but unblock the situations where we have control over the base class.

On the other hand, this issue is still important for classes where we don't have the ability to modify the implementation class, such as:

It's also important for situations where we want that generic parameter to be the type of the result of a function:

Param example<T extends Foo<var Param>>() {
  ...
} 
felangel commented 4 years ago

@rrousselGit thanks for the suggestion but unless I'm misunderstanding I don't think this workaround applies when you have a generic parameter which extends a class that has another generic parameter.

class Foo<T> {
  const Foo(this.value);
  final T value;
}

class Bar<T extends Foo<S>, S> {
  Bar({this.foo, this.baz}) {
    baz(foo.value);
  }

  final T foo;
  void Function(S s) baz;
}

void main() {
  final foo = Foo<int>(0);
  final bar = Bar(
    foo: foo,
    baz: (s) {
      // s is dynamic
    },
  );
}

In the above example, I can't seem to force Dart to resolve the type of S in class Bar. It correctly resolves T in class Bar as Foo<int> but within Bar the function baz still interprets S as dynamic unless you explicitly specify the types

Bar<Foo, int>(
  foo: foo,
  baz: (s) {
    // s is an int
  },
);
escamoteur commented 4 years ago

I hit something similar today

  R watch<T extends ValueListenable<R>, R>({String instanceName}) =>
      state.watch<T, T>(instanceName, null).value;

When only providing T the compiler could not infer R so that you always have to provide both. It would be helpful if the type parameters would be evaluated from left to right so that you can omit some that then will get inferred.

daniel-mf commented 3 years ago

@escamoteur I just hit that too and it's not the first time. In TypeScript we have the infer keyword which addresses similar situations. Any chances of getting this further?

matejthetree commented 3 years ago

I am looking for the same thing.

Here is a simple example that could be infered but throws on runtime

class IterableManager<S extends Iterable<T>, T> {
  IterableManager({required this.iterable});

  final S iterable;

  T getIndex(int index) => iterable.elementAt(index);
}

void main() {
  var manager = IterableManager(iterable: List.generate(10, (index) => index));
  double index = manager.getIndex(3);
}

Inference shows that IterableManager is of List<int>,dynamic> where it could know that T is int

Best imo would be to allow the generic to be declared inside extend like IterableManager<S extends Iterable<T>>

eernstg commented 3 years ago

You're encountering the issue that type inference in Dart does not support flow of information from one actual argument to another, cf. https://github.com/dart-lang/language/issues/731.

By the way, can you give a hint about why you wouldn't be happy with the following?:

class IterableManager<T> {
  IterableManager({required this.iterable});
  final Iterable<T> iterable;
  T getIndex(int index) => iterable.elementAt(index);
}

void main() {
  var manager = IterableManager(iterable: List.generate(10, (index) => index));
  double index = manager.getIndex(3);
}
leafpetersen commented 3 years ago

You're encountering the issue that type inference in Dart does not support flow of information from one actual argument to another, cf. #731.

@eernstg this has nothing at all to do with #731. There are no arguments here for information to flow through. IterableManager says that it needs S <: Iterable<T>, and the IterableManager constructor call constrains S <: Iterable<int> to be true. There are no constraints on T at all, and choosing any top type for T satisfies the constraints on S, so we have a perfectly good solution.

There are other things you can try to do to incorporate information from the bounds in order to derive a "tighter" constraint, but when I've looked into this, I've found that no solution dominates: for any heuristic you pick, you make some patterns work, and others fail.

For these kind of patterns where you're trying to "program" the generics in order to extract sub-components of inferred types, I believe the solution that I pointed to in my comment above is probably the right direction.

eernstg commented 3 years ago

this has nothing at all to do with #731. ...

Right, thanks!

JohnGalt1717 commented 3 years ago

I have this problem all the time.

For instance:

class ModularRoute<TPageParameters extends PageParameters,
        TModularPage extends ModularPage<TPageParameters>>
    extends BaseModularRoute<TPageParameters, TModularPage> {
  ModularRoute({
    required BaseModule module,
    required String route,
    required TModularPage Function(Map<String, String?> params) createPage,
    FutureOr<bool> Function(
      ModularHistory route,
      ModularRouterDelegate delegate,
    )?
        guard,
    bool overrideModuleGuard = false,
  }) : super(
          module: module,
          route: route,
          createPage: createPage,
          guard: guard,
          overrideModuleGuard: overrideModuleGuard,
        );
}

Dart completely fails to infer the types even though they're obviously provided in the parameters of the constructor. (i.e. since TModularPage is defined and includes TPageParameters in what it extends, then we know both what TModularPage is and TPageParameters based on the createPage method if the createPage function is in lamda notation.

But let's say that it isn't in lamda notation or you have a case where you can't infer the types for the generic.

This class would be better expressed as:

class ModularRoute<TModularPage extends ModularPage<TPageParameters extends PageParameters>> extends BaseModularPage<TModularPage> {}

(BaseModularPage has the same problem)

Thus used like this:

final route = ModularRoute<SomeModularPage>(...);

This explicitly states that there are 2 generics that define this class that can be used in the class AND that they can both be found by the single definition because the extension is itself is limited to a generic type that defines the other parameter.

We would then be able to use TModularPage and TPageParameters strongly throughout our code without repeating ourselves over and over again for something that is clearly in the generic definition this way.

From what I can tell, all other options above either only work if you know the types (which you don't necessarily know if it's being derived from a function) or you add other verbosity to your creation of the object.

daniel-mf commented 1 year ago

Many years waiting for this, with many duplicate issues posted. Any changes we see a solution in the near future?

pedromassango commented 1 year ago

It has been sooo long that I forgot how bad the Flutter community needs this.

13thdeus commented 1 year ago

I needed to select class by its generic type. Here is code based on @rrousselGit solution

class Par {}

class Par1 extends Par {}

class Par2 extends Par {}

class Api<TSub> {
  R _capture<R>(R Function<TSub>() cb) => cb<TSub>();

  getItemRuntimeType() => _capture(<P>() => P);

  @override
  toString() => 'Base Api for ${getItemRuntimeType()}';
}

class Par1Api extends Api<Par1> {
  @override
  toString() => 'Par1Api';
}

class Par2Api extends Api<Par2> {
  @override
  toString() => 'Par2Api';
}

void main() {
  check(Par1(), [Par1Api(), Par2Api()]); // targets: [Par1Api]
  check(Par2(), [Par1Api(), Par2Api()]); // targets: [Par2Api]

  check(Par1(), [Par1Api(), Par2Api(), Api<Par1>(), Api<Par2>()]); // targets: [Par1Api, Base Api for Par1]
  check(Par2(), [Par1Api(), Par2Api(), Api<Par1>(), Api<Par2>()]); // targets: [Par1Api, Base Api for Par1]

  check(Par1(), [Api<Par1>(), Api<Par2>()]); // targets: [Base Api for Par1]
  check(Par2(), [Api<Par1>(), Api<Par2>()]); // targets: [Base Api for Par2]
}

void check(Par target, List<Api> checkers) {
  final targetClass =
      checkers.where((e) => e.getItemRuntimeType() == target.runtimeType);

  print('targets: [${targetClass.map((t) => t.toString()).join(', ')}]');
}
Tienisto commented 1 year ago

As a riverpod user, this is so much needed. It always infers dynamic although it should be able to infer...

// inferred as NotifierProviderImpl<SettingsNotifier, SettingsState>
final settingsProvider = NotifierProvider<SettingsNotifier, SettingsState>(() {
  return SettingsNotifier();
});

// inferred as NotifierProviderImpl<SettingsNotifier, dynamic>
final settingsProvider2 = NotifierProvider(() {
  return SettingsNotifier();
});

class SettingsNotifier extends Notifier<SettingsState> {
  // ...
}

You don't notice the missing generics right away, but as soon as you read it with ref.read or ref.watch, you notice that autocompletion does not work anymore because you are essentially work with dynamic.

rrousselGit commented 1 year ago

It always infers dynamic although it should be able to infer...

Note that there are various ways to catch things

Numerous lints offer a warning if type inference defaults to dynamic. There are also some "strict" options such as strict-raw-types which can help.

JohnGalt1717 commented 1 year ago

It always infers dynamic although it should be able to infer...

Note that there are various ways to catch things

Numerous lints offer a warning if type inference defaults to dynamic. There are also some "strict" options such as strict-raw-types which can help.

The issue is that you're adding endless extra code that isn't needed. Dart should immediately know that this isn't dynamic and what the type is. I shouldn't have to repeat myself (i.e. this violates DRY).

And while strict raw types catches this most of the time, there are cases where it won't and you're creating runtime bugs as a result. The language server and dart itself should catch this at compile time, know the type and validate everything strongly because there is absolutely no posibility that it can be anything other than the obvious.

eernstg commented 1 year ago

The ability to perform pattern matching on type arguments could be a somewhat complex feature, so maybe we should consider a simpler approach. I proposed a "type function" based approach in https://github.com/dart-lang/language/issues/3324.

Using that approach, we could handle the original example here as follows (with a slight enhancement, because we need to use TEvent in BlocBuilder in order to have a need for the type parameter TEvent in the first place):

// We can write this today.

class Bloc<TEvent, TState> {}

class BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> {
  TState get g => ...;
}

// We would like to omit the type argument for `TState` in client code, so we use the new feature.

class Bloc<TEvent, TState> {}

class BlocBuilder<TBloc extends Bloc> {
  ImplementsAt2<TBloc, Bloc> get g => ...;
}

The point is simply that we can express the type which is the type argument S2 of Bloc for the type Bloc<S1, S2> that TBloc implements: It is denoted by the "magic" type ImplementsAt2<TBloc, Bloc>.

In other words, we have a type TBloc which is known to be a subtype of Bloc (that is, TBloc <: Bloc<Object?, Object?>). Moreover, TBloc implements Bloc<S1, S2> for some types S1 and S2, and then ImplementsAt2<TBloc, Bloc> is simply S2.

@Tienisto wrote:

As a riverpod user, this is so much needed.

I assume the declaration would this one (found here):

typedef NotifierProvider<NotifierT extends Notifier<T>, T>
    = NotifierProviderImpl<NotifierT, T>;

We would then use this instead:

typedef NotifierProvider<NotifierT extends Notifier>
    = NotifierProviderImpl<NotifierT, ImplementsAt1<NotifierT, Notifier>>;

which would allow this

// We can provide the type argument explicitly.
final settingsProvider = NotifierProvider<SettingsNotifier>(() => SettingsNotifier());

// But it is already inferred as `SettingsNotifier`, which should still work.
final settingsProvider2 = NotifierProvider(() => SettingsNotifier());
Tienisto commented 1 year ago

@eernstg Regarding your Riverpod / provider proposal: We cannot reduce the generic count to one because both generic types are needed. We need to access the notifier (1) and we also need to access its state (2).

final a = ref.read(settingsProvider.notifier); // returns NotifierT
final b = ref.read(settingsProvider); // returns T

if settingsProvider is typed with NotifierT, we lose the T. So, b will be inferred as dynamic I guess.

eernstg commented 1 year ago

We cannot reduce the generic count to one because both generic types are needed

The proposal in https://github.com/dart-lang/language/issues/3324 allows us to do exactly that: We declare NotifierT as a type parameter, as before, but we introduce a way to denote the associated value of T explicitly as a "type expression".

In particular, NotifierProvider<SettingsNotifier> is a type alias for the type NotifierProviderImpl<SettingsNotifier, SettingsState>, because the value of the type expression ImplementsAt1<SettingsNotifier, Notifier> is SettingsState.

final a = ref.read(settingsProvider.notifier); // `a` has inferred type `NotifierT`.
final b = ref.read(settingsProvider); // `b` has inferred type `ImplementsAt1<NotifierT, Notifier>`.

So the inferred type of b is SettingsState if NotifierT is known to have the value SettingsNotifier.

I mentioned the possibility that we could combine this with the introduction of optional type parameters. This would give you a little more control, because we could choose "T" to have a value which is not minimal (but which would still ensure that all bounds are satisfied):

typedef NotifierProvider<NotifierT extends Notifier<T>, T = ImplementsAt1<NotifierT, Notifier>>
    = NotifierProviderImpl<NotifierT, T>;

If we do it like that then we will have access to T as a regular type variable on the right hand side of the type alias. You would then be able to use NotifierProvider<SettingsNotifier, dynamic> if you actually want to have a more general value for T. But if you use it like NotifierProvider<SettingsNotifier> then T will get the value SettingsState based on the declared default value.

rrousselGit commented 1 year ago

Sounds good to me.

Honestly I don't think it's that common is user code. It's likely mainly packages which are impacted, for usability reasons. So I don't care too much about the syntax of the feature and don't mind swallowing the cost.

eernstg commented 5 months ago

Not much progress here for a while. However, I'll mention one technique that could be helpful. It is safe in the sense that the otherwise unconstrained type variable will get the value which is requested in this issue, but it is inconvenient because the type arguments generally have to be specified explicitly.

I think the difficulty that gave rise to this issue in the first place can be described by the following example:

class Bloc<TEvent, TState> {}

class BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> {
  BlocBuilder();
  BlocBuilder.fromTBloc(TBloc bloc) {
    print('BlocBuilder<$TBloc, $TState>');
  }
}

class TestBloc extends Bloc<String, String> {}

class TestBlocBuilder extends BlocBuilder<TestBloc, String> {}

void main() {
  // 2nd type argument just needs to be a supertype of `String`.
  BlocBuilder<TestBloc, String>(); // OK.
  BlocBuilder<TestBloc, Object>(); // OK.
  BlocBuilder<TestBloc, dynamic>(); // OK.
  // .. but we want `String`, to preserve the information.

  // Inference from `TBloc` yields `dynamic` for `TState`.
  BlocBuilder.fromTBloc(TestBloc()); // 'BlocBuilder<TestBloc, dynamic>'.
  // .. which is the worst possible choice.
}

The point is that BlocBuilder<TestBloc, T> satisfies the declared bounds for absolutely any T which is a supertype of the type that we actually want, namely String. This means that we can manually specify "bad" types like BlocBuilder<TestBloc, dynamic>, and it means that an inferred type argument for TState will also be dynamic.

However, we can tie TState to the actual type argument of Bloc by making Bloc invariant in TState. With that, we get the following example:

// Using statically checked invariance, needs
// `--enable-experiment=variance`.

class Bloc<TEvent, inout TState> {}

class BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> {
  BlocBuilder();
  BlocBuilder.fromTBloc(TBloc bloc) {
    print('BlocBuilder<$TBloc, $TState>');
  }
}

class TestBloc extends Bloc<String, String> {}

class TestBlocBuilder extends BlocBuilder<TestBloc, String> {}

void main() {
  // 2nd type argument must match the type argument of `TestBloc`.
  BlocBuilder<TestBloc, String>(); // OK.
  BlocBuilder<TestBloc, Object>(); // Compile-time error.
  BlocBuilder<TestBloc, dynamic>(); // Compile-time error.

  // Inference from `TBloc` fails.
  BlocBuilder.fromTBloc(TestBloc()); // Compile-time error.

  // We can specify the type argument manually,
  // and only `String` is allowed with `TestBloc`.
  BlocBuilder<TestBloc, String>.fromTBloc(TestBloc());
}

We can emulate invariance in the current language:

typedef Inv<X> = X Function(X);
class _Bloc<TEvent, TState, Invariance extends Inv<TState>> {}
typedef Bloc<TEvent, TState> = _Bloc<TEvent, TState, Inv<TState>>;

// ... remaining code is unchanged.

Of course, this technique can only be used in the case where Bloc can actually be invariant in TState, which may be desirable, acceptable, inconvenient, or impossible, depending on what's in the body of Bloc, and how it is used.

eernstg commented 3 months ago

tl;dr   A couple of solutions, if you're willing to adjust your code slightly and we get #3009    rd;lt

Note that there is ongoing work in the area of type inference as mentioned in https://github.com/dart-lang/language/issues/3009#issuecomment-2211022064. With that generalization we'd have the following:

// When the type inference improvement is enabled.

class Bloc<TEvent, TState> {}

class BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> {
  BlocBuilder();
  BlocBuilder.fromTBloc(TBloc bloc) {
    print('BlocBuilder<$TBloc, $TState>');
  }
}

class TestBloc extends Bloc<String, String> {}

class TestBlocBuilder extends BlocBuilder<TestBloc, String> {}

void main() {
  // Inference from `TBloc` yields `String` for `TState`.
  BlocBuilder.fromTBloc(TestBloc()); // Prints 'BlocBuilder<TestBloc, String>'.
  // .. which is what we want.
}

This will handle the expression inference case as desired, which is the case that I was focusing on here.

However, it does not provide a solution for the situation described in the first posting of this issue because that was a case where inference doesn't occur at all (so it doesn't help us that inference is improved).

However, we can still solve the task in the original posting if we change the type hierarchy slightly, namely by changing one of the classes to be a mixin:

abstract class FixBloc<TBloc> {}

class Bloc<TEvent, TState> {}
mixin BlocBuilder<TBloc extends Bloc<dynamic, TState>, TState> on FixBloc<TBloc> {}

class TestBloc extends Bloc<String, String> {}
class TestBlocBuilder extends FixBloc<TestBloc> with BlocBuilder {}

In this example, the superclass BlocBuilder is turned into a mixin. This is useful because we have a special feature known as mixin inference which is able to perform the same job as expression inference did in the previous example.

The idea is that we're communicating to the mixin that it must have a particular actual type argument S for the first type parameter (TBloc), which is achieved by having a shared superinterface FixBloc<S>. In this case we have FixBloc<TestBloc> as a superclass of TestBlocBuilder, which implies that there is no solution to the task that mixin inference is handling unless it chooses Testbloc as the first type argument to BlocBuilder. You could say that FixBloc is used to pass exactly one type argument to BlocBuilder. Mixin inference then proceeds to select String as the second type argument to BlocBuilder.