Skip to content

Infer generic type parameters based on the declaring type #620

Open
@bigworld12

Description

@bigworld12

cross reference : felangel/bloc#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.

Activity

leafpetersen

leafpetersen commented on Oct 11, 2019

@leafpetersen
Member

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

bigworld12 commented on Oct 11, 2019

@bigworld12
Author

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

leafpetersen commented on Oct 11, 2019

@leafpetersen
Member

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.

added
requestRequests to resolve a particular developer problem
on Oct 11, 2019
bigworld12

bigworld12 commented on Oct 11, 2019

@bigworld12
Author

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

leafpetersen commented on Oct 11, 2019

@leafpetersen
Member

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

bigworld12 commented on Oct 11, 2019

@bigworld12
Author

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

bigworld12 commented on Oct 11, 2019

@bigworld12
Author

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

leafpetersen commented on Oct 11, 2019

@leafpetersen
Member

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

bigworld12 commented on Oct 17, 2019

@bigworld12
Author

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

eernstg

eernstg commented on Oct 18, 2019

@eernstg
Member

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

rrousselGit commented on Jan 25, 2020

@rrousselGit

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:

  • Stream
  • ValueNotifier

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

felangel commented on Jan 27, 2020

@felangel

@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
  },
);

32 remaining items

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    requestRequests to resolve a particular developer problem

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @JohnGalt1717@daniel-mf@leafpetersen@felangel@eernstg

        Issue actions

          Infer generic type parameters based on the declaring type · Issue #620 · dart-lang/language