dart-lang / language

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

allow more compact definition of generic classes with type parameters that extend other generic classes #102

Open rbellens opened 5 years ago

rbellens commented 5 years ago

When defining a generic class which has a generic parameter that should extend a type that in turn is a generic class, you now have to add all the generic parameters of the second type also in the parameter list of the first class to be able to access those type parameters. This is cumbersome and seems unnecessary.

For example, the following definition:

class A<T extends B<U,V,W>,U,V,W> {
  U u;
}

could be simplified to:

class A<T extends B<U,V,W>> {
  U u;
}

I don't think that allowing this syntax would have any implications or breaking changes, it would simply be syntactic sugar for the first syntax.

A concrete example is the following situation. Suppose you are using the BLoC pattern and have a base class for blocs BaseBloc which has a generic parameter that defines the state:

abstract class BaseBloc<S> {
  Stream<S> get state;
}

and a base class for components in AngularDart (or widgets or flutter):

// in the current syntax the extra type parameter S is still necessary
class BaseBlocComponent<T extends Bloc<S>/*,S*/> { 
  final T bloc;

  S currentState;

  BaseBlocComponent(this.bloc) {
    bloc.stream.listen((state)=>currentState = state);
  }

}

You could now implement a concrete state, bloc and component:


class MyState {}

class MyBloc extends BaseBloc<MyState> {

}

class MyComponent extends BaseBlocComponent<MyBloc/*,MyState*/> {
// The extra MyState parameter is unnecessary, because it is implied by the type of MyBloc
// Moreover, you need to know which classes MyBloc extends in order to be able to set the
// correct value for the second type parameter

}
lrhn commented 5 years ago

A problem with the

class A<T extends B<U,V,W>> {
  U u;
}

syntax is that U, V, and W may now be defined in the surrounding scope. If, say, V is defined as a type in the surrounding scope, then the meaning would change to

class A<T extends B<U, V, W>, U, W> {
  U u;
}

which only takes three type arguments. Such "breaking at a distance" is a sign of a fragile language feature design.

Readability-wise, a reader has no clue (except the single-letter names) that U, V and W are supposed to be new type variables (we don't introduce them like we normally do, so we cannot see where they are bound). A cursory reading would read A as a class taking one type argument, not four. A cursory reading is more than most people give source code that they are not writing themselves.

I think this would be sacrificing significant readability for a very minor gain in characters written.

rbellens commented 5 years ago

I understand that it is important to have all type parameters introduced in the definition of class A and this is indeed little extra work. The issue lies in the usage of class A.

Suppose, you have a class C which extends B. Maybe, this class C is defined in a third party library, so the user has no immediate knowledge about the bounds of U and W. So, when defining a variable a, he/she does not know which type variables to use:

A<C,?,?> a;

Maybe, instead of changing the syntax of class definitions, this situation can also be tackled by extending the possibility of optional type arguments or on the level of the analyzer.

Now, if I am not mistaken, type arguments are optional, but you have to either write none or all of them.

Suppose you have the following class:

class A<T extends num> {
  T value;
}

Then, when you define a variable a of type A (without type arguments), the analyzer will know that a.value is a num and as a developer you can profit from code completion and error detection.

Now, suppose you have the following classes:

class A<T extends B<U, V, W>, U, W> {
  U u;
}

class C extends B<int, bool, String> {
}

If you now define a variable a of type A with the first type argument C, you either have to add the int and String

A<C,int,String> a;

or replace them with dynamic (or Object)

A<C,dynamic,dynamic> a;

In the latter case, you can not profit from code completion or error detection, because the analyzer does not seem to derive the bounds on U and W, although they are bounded. So, a first step could be to be able to use dynamic and let the analyzer derive proper bounds.

Maybe, writing dynamic does not really reflect the intention as it can not be just any type, but only int and String respectively. So, maybe, instead of writing dynamic, those type arguments could be made optional, so that every absent type argument is replaced with its bound as is now already the case when you do not write any type argument.

wrozwad commented 5 years ago

I have another example where I want to have "optional generics":

B useBloc<B extends Bloc<T>, T>([T initialValue]) =>
    Hook.use(_BlocHook<B>(initialValue));

The usage of this function is:

useBloc<WithInitialValueBloc, int>(5);
useBloc<WithoutInitialValueBloc, void>();

I think that It'll be much better if I can use above functions without syntactic sugar:

useBloc<WithInitialValueBloc>(5); // function will accepts only `int` arguments implicitly
useBloc<WithoutInitialValueBloc>();
eernstg commented 5 years ago

There is one issue with the following setup:

abstract class BaseBloc<S> { Stream<S> get stream; }
abstract class Bloc<S> extends BaseBloc<S> {} // Guessing.

class BaseBlocComponent<T extends Bloc<S>/*,S*/> { 
  final T bloc;
  S currentState;
  BaseBlocComponent(this.bloc) {
    bloc.stream.listen((state) => currentState = state);
  }
}

class MyState {}
class MySmartState implements MyState {}
class MyBloc extends Bloc<MySmartState> { get stream => null; }

class MyComponent extends BaseBlocComponent<MyBloc/*, MyState*/> {
  MyComponent(MyBloc bloc): super(bloc);
}

If you allow the type argument S to be omitted then it is not available for the declaration of currentState. So how do you deal with BaseBlocComponent<Null>? Null is a subtype of Bloc<S> for all S (come non-nullable types you'll use Never or add a ?).

Also, you can pass type arguments like BaseBlocComponent<MyBloc, MyState> as well as BaseBlocComponent<MyBloc, MySmartState>, so the value of the second type argument is already not uniquely determined.

But if you always want to use a specific type for the type parameter whose bound relies on the remaining type parameters then you could just invert the pattern:

class BaseBlocComponent<S> { 
  final Bloc<S> bloc;  // <----- Inline former type parameter `T` here!
  S currentState;
  BaseBlocComponent(this.bloc) {
    bloc.stream.listen((state) => currentState = state);
  }
}

class MyComponent extends BaseBlocComponent<MyState> {
  MyComponent(MyBloc bloc): super(bloc);
}

So do you actually need to know that the bloc has a more specific type than Bloc<S>?

If you really do need that, then I think the most workable idea would be to allow partial instantiation to bound (this is a possible extension to the current instantiation to bound mechanism):

class C<X extends num> {}
class D<X extends D<X>> {}
class BaseBlocComponent<T extends Bloc<S>, S> {}

// Regular instantiation to bound.
C c; // Means `C<num>`.
D d; // Means `D<D<dynamic>>`.

// Now the partial kind: Type argument `_` is "explicitly omitted".
class Foo extends BaseBlocComponent<_, MyState> {}

Foo would then be a subclass of BaseBlocComponent<Bloc<MyState>, MyState>. It wouldn't magically give you everything (e.g., BaseBlocComponent<MyBloc, _> would mean BaseBlocComponent<MyBloc, dynamic> rather than BaseBlocComponent<MyBloc, MySmartState>, because this is still instantiation to bound, not "do what I mean" ;-), but it might still be a way to get a more concise notation for many useful outcomes.

PS: Instantiation to bound might change to use Object? rather than dynamic in the future as the "default default", and with a new feature like _ it could certainly do so without breaking existing code. So there's nothing inherently type-unsafe about partial instantiation to bound.

TimWhiting commented 3 years ago

Seems like similar to #620