Skip to content

Allow static factory methods to work more like constructorsΒ #4539

@eernstg

Description

@eernstg

Consider the following program:

class A<X> {
  final X x;
  A.one(this.x);
  static A<X> two<X, Y>(X Function(Y) f, Y y) => A.one(f(y));
}

void main() {
  // Works.
  A<num> _ = A.one(1);
  A<num> _ = A.two((s) => s.length, 'Hello!');

  A<num>.one(1); // Works.
  A<num>.two((s) => s.length, 'Hello!'); // Error.
}

This illustrates the fact that a constructor invocation like (A.one(1)) can use the context type (A<num>) to infer the actual type argument which needs to be passed (as in A<num>.one(1)).

Similarly, a static generic method (like A.two) can use the context type plus information from inference on the actual arguments to infer the actual type arguments (A.two<num, String>((s) => s.length, 'Hello!')).

The static method can do something that the constructor can not do: It can declare a type parameter of its own, in addition to the type parameters of the returned object that we're passing to the class name in an expression like A<num>.one(1). Such "extra" type parameters can be used to specify that there must be some sort of correspondence between the actual argument types. In the example, the first argument f needs to be a function which can accept the second argument y and return a value whose type is the actual type argument of the given instance of A. This is safer than using Object? or dynamic rather than Y.

This is exactly the kind of expressive power that generic constructors can provide (e.g., #4213). However, except for being constant, static methods are already able to do everything that a generic constructor can do.

One distinction still reveals that we're calling a static method rather than a generic constructor: The constructor invocation receives type arguments determining the type of the returned object on the class (as in A<num>.two(...) rather than A.two<num>(...)), and there may be "extra" type arguments which are concerned with the invocation rather than the returned object (such as String in the actual invocation A.two<num, String>(...)).

It can be inconvenient that the type parameters are passed together, especially if the "extra" type arguments are verbose and complex, but type inference would actually find them, but we must specify all the type arguments because we need to specify the ones for the class.

This issue is a proposal that we allow invocations like A<num>.two((s) => s.length, 'Hello!') and the meaning of this construct is that the invocation A.two((s) => s.length, 'Hello!') is inferred with context type A<num>, and the static type of the expression is A<num>.

It is a compile-time error if this inference step yields actual type arguments such that the actual return type of A.two is not a subtype of the specified type A<num>.

(In particular, this feature is only applicable when a static method is a "factory method" in the sense that it returns an instance of the enclosing class or some subtype thereof.)

This would allow us to specify only the type arguments of the returned object, and leave it to the type inference algorithm to check that the typing correspondences between the actual arguments are satisfied.

We may not wish to allow passing actual type arguments in both positions (A<num>.two<String>(...) or something like that). It is possible to specify rules to allow this, but it might not be useful. If there is a need to pass the "extra" type arguments rather than getting them inferred then we should simply call the static method in the normal way (like A.two<int, String>(...)).

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureProposed language feature that solves one or more problems

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions