dart-lang / language

Design of the Dart language
Other
2.65k stars 202 forks source link

Let a wildcard as an actual type argument indicate a request for inference #3963

Open eernstg opened 2 months ago

eernstg commented 2 months ago

This is a proposal for supporting expressions like MyClass<int, _>('Hello'), where _ is used to indicate that the corresponding type parameter should be obtained from type inference. This allows us to specify some type arguments whose value would otherwise not fit the needs, and omit other type arguments whose inferred value is as desired. For example:

class A<X, Y> {
  final X x;
  final _ys = <Y>[];
  A(x);
  void add(X Function(Y) g) => _ys.add(g(x));
}

// Assume that we want to create an `A<int, String>`.
void main() {
  // We can specify all type arguments, ..
  var a1 = A<int, String>(42); // .. but `int` is redundant.

  // We can infer all type arguments, ..
  var a2 = A(42); // .. but `Y` is now `dynamic`.

  // With this proposal, we get the best of two worlds.
  var a3 = A<_, String>(42);
}

We could allow a wildcard type argument that occurs as the last element of the actual type argument list to stand for multiple type arguments. For example, this would allow us to use C<T1, _> to stand for C<T1, T2, T3> where both T2 and T3 are inferred.

There should not be a large amount of expressive power in this feature, but it does take a few steps to emulate it in the current language.

We can get the same effect today if we're willing to create a type alias for each choice of fixed type arguments, and if those type arguments can be denoted globally:

// Same `class A`.

typedef AHelper<X> = A<X, String>;

void main() {
  // Emulate `A<_, String>(42)`.
  var a4 = AHelper(42); // Creates an `A<int, String>` and infers that type for `a4`.
}

We can also use a function to support arbitrary choices of "fixed" type arguments:

// Same `class A`.

// Create a context where we can denote a type which isn't denotable globally.
void foo<Z>() {
  // Emulate `A<_, Z>(42)`.
  // Note that we can't declare `typedef AHelper<X> = A<X, Z>;`
  // because type aliases are global.

  // We can still specify all type arguments, brute force.
  var a5 = A<int, Z>(42);

  // However, we want to avoid specifying `int` because it can be inferred. So we
  // use a local function that has the choice of `Z` as 2nd type argument baked in.
  A<V, Z> local<V>(V v) => A(v);
  var a6 = local(42);
}

void main() => foo<String>();

It is a new feature to include a special (wildcard-ish) treatment of _ when it is used as an actual type argument. It is also (slightly) breaking because it is possible today to declare some entities whose name is _ (e.g., top-level variables or, indeed, type parameters).

However, I think it's more helpful to allow developers to request this kind of partial type inference by means of _ than it is to preserve the ability to use a type whose name is _ as an actual type argument.

In the end, it would actually be possible to create a type alias to provide access to such types under a different name:

class _ {}
typedef AMuchBetterName = _;
List<AMuchBetterName> list = []; // Can't use `List<_>`, but this is better, anyway.
rrousselGit commented 2 months ago

Consider a more complex example:

class A<T> {}

class B<FirstT extends A<SecondT>, SecondT> {}

If we write:

B<A<int>, _>

Would this infer B<A<int>, int>?

eernstg commented 2 months ago

Current type inference would not generate any constraints on SecondT, so it ends up being a top type. Here's a variant of the example that shows the outcome:

class A<X> {
  A(X x);
}

class B<Y extends A<Z>, Z> {
  B(Y a) {
    print('B<$Y, $Z>');
  }
}

void main() {
  B(A(42)); // 'B<A<int>, dynamic>'.
}

This is a valid result from type inference because binding Z to a top type is actually a choice that allows us to satisfy all the subtype constraints.

However, we're currently working on an improvement of the type inference algorithm (see https://github.com/dart-lang/language/issues/3009) that gives rise to additional constraints. This will allow us to infer a tighter solution, namely B<A<int>, int>.

So the proposal in this issue would allow us to ask for inference, and we'd then rely on other enhancements (of type inference itself) in order to get the more tight result.

PS: Yes, #3009 is about F-bounded type parameters, but the solution that we're currently evaluating is more general than F-bounds, and in particular it does allow us to bind Z to int in this example.