Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow factory constructors to specify a return type #4153

Open
eernstg opened this issue Nov 5, 2024 · 0 comments
Open

Allow factory constructors to specify a return type #4153

eernstg opened this issue Nov 5, 2024 · 0 comments
Labels
brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form enhanced-const Requests or proposals about enhanced constant expressions feature Proposed language feature that solves one or more problems

Comments

@eernstg
Copy link
Member

eernstg commented Nov 5, 2024

Constructors generally don't specify a return type because they will return an instance of the enclosing class, so it's redundant.

However, this is not the whole truth when it comes to factory constructors. They may in general return an instance of an arbitrary subtype of the enclosing class. For example:

abstract class A {
  const A._();
  const factory A() = _AImpl;
}

class _AImpl extends A {
  const _AImpl(): super._();
}

This is a typical use case, and the point is that the class A provides a public (possibly widely used) interface, and the actual implementation is provided by one or more subtypes like _AImpl. In general, client code does not need (or want!) to depend on those subtypes, so they are removed from public access (and may even be private) and client code only gets to use them because they can call some redirecting factories like A.

However, we can also have situations where the subclasses are public in nature, and we'd like to combine the ability to call constructors declared by the supertype, and still have access to the interface of each of the subtypes. In this case we could allow the redirecting factory constructor to declare a return type (which must be a subtype of the type of the class, including any type arguments as declared):

abstract class A<X> {
  const A._();
  const factory B<X> A() = B;
}

class B<X> extends A<X> {
  int i = 0;
  const B(): super._();
}

void main() {
  A<String>()..i = 1;
}

It may seem contradictory to wish to have the interface of B<T> in an expression that looks like A<T>() for some T. However, the discussion in #357 has repeatedly brought up a situation where it may be convenient, namely the case where an expression like EdgeInsetsGeometry.all(16.0) is abbreviated to .all(16.0) based on having EdgeInsetsGeometry as the context type.

The idea is that we provide constructors of subtypes like EdgeInsets and EdgeInsetsDirectional as constructors of EdgeInsetsGeometry. That's nothing new, but we do want to have access to members of those subclasses, and this will only work if we can declare the return type:

abstract class EdgeInsetsGeometry {
  const EdgeInsetsGeometry();

  // Forward to EdgeInsets:
  const factory EdgeInsets EdgeInsetsGeometry.fromLTRB(
    double left,
    double top,
    double right,
    double bottom,
  ) = EdgeInsets.fromLTRB;

  const factory EdgeInsets EdgeInsetsGeometry.all(double value) = EdgeInsets.all;

  const factory EdgeInsets EdgeInsetsGeometry.only({
    double left,
    double top,
    double right,
    double bottom,
  }) = EdgeInsets.only;

  const factory EdgeInsets EdgeInsetsGeometry.symmetric({
    double vertical,
    double horizontal,
  }) = EdgeInsets.symmetric;

  static const zero = EdgeInsets.only();

  // Forward to EdgeInsetsDirectional:
  const factory EdgeInsetsDirectional EdgeInsetsGeometry.fromSTEB(
    double start,
    double top,
    double end,
    double bottom,
  ) = EdgeInsetsDirectional.fromSTEB;

  const factory EdgeInsetsDirectional EdgeInsetsGeometry.onlyDirectional({
    double start,
    double top,
    double end,
    double bottom,
  }) = EdgeInsetsDirectional.only;

  const factory EdgeInsetsDirectional EdgeInsetsGeometry.symmetricDirectional({
    required double horizontal,
    required double vertical,
  }) = EdgeInsetsDirectional.symmetric;

  const factory EdgeInsetsDirectional EdgeInsetsGeometry.allDirectional(double value) =
      EdgeInsetsDirectional.all;

  static const zeroDirectional = EdgeInsetsDirectional.only();
}

A couple of brevity features (#4135 and #4144) would allow us get exactly the same behavior with less noise:

abstract class EdgeInsetsGeometry {
  const .new();

  // Forward to EdgeInsets:
  const factory EdgeInsets .fromLTRB = EdgeInsets.fromLTRB;
  const factory EdgeInsets .all = EdgeInsets.all;
  const factory EdgeInsets .only = EdgeInsets.only;
  const factory EdgeInsets .symmetric = EdgeInsets.symmetric;

  static const zero = EdgeInsets.only();

  // Forward to EdgeInsetsDirectional:
  const factory EdgeInsetsDirectional .fromSTEB = EdgeInsetsDirectional.fromSTEB;
  const factory EdgeInsetsDirectional .onlyDirectional = EdgeInsetsDirectional.only;
  const factory EdgeInsetsDirectional .symmetricDirectional = EdgeInsetsDirectional.symmetric;
  const factory EdgeInsetsDirectional .allDirectional = EdgeInsetsDirectional.all;

  static const zeroDirectional = EdgeInsetsDirectional.only();
}

We might even say that if a redirecting factory constructor declares a return type then it will serve as the default class name in the redirection:

abstract class EdgeInsetsGeometry {
  const .new();

  const factory EdgeInsets .fromLTRB = .fromLTRB;
  const factory EdgeInsets .all = .all;
  const factory EdgeInsets .only = .only;
  const factory EdgeInsets .symmetric = .symmetric;

  static const zero = EdgeInsets.only();

  const factory EdgeInsetsDirectional .fromSTEB = .fromSTEB;
  const factory EdgeInsetsDirectional .onlyDirectional = .only;
  const factory EdgeInsetsDirectional .symmetricDirectional = .symmetric;
  const factory EdgeInsetsDirectional .allDirectional = .all;

  static const zeroDirectional = EdgeInsetsDirectional.only();
}

Note that . is punctuation, which means that the parser will have no difficulty parsing the declaration with no space before the ., in case that's the preferred style (as in const factory EdgeInsets.all = .all;).

The point is that we can now tailor the static namespace of EdgeInsetsGeometry such that it offers constructors of various subtypes of that class (such that we can use the .identifier(...) syntax to invoke it when the context type is EdgeInsetsGeometry). It preserves the ability to use the enhanced interface of the given subtype in the instance creation expression itself (as in .all(10).copyWith(right: 15)), and it preserves the ability to be constant.

Finally, non-redirecting factory constructors could also allow for a declared return type:

abstract class A {
  A._();
  factory B A(int x) => B(x + 1);
}

class B extends A {
  final int x;
  B(this.x): super._();
}

This could be used with the .identifier syntax as well, for similar reasons.

@eernstg eernstg added feature Proposed language feature that solves one or more problems brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form enhanced-const Requests or proposals about enhanced constant expressions and removed brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form labels Nov 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
brevity A feature whose purpose is to enable concise syntax, typically expressible already in a longer form enhanced-const Requests or proposals about enhanced constant expressions feature Proposed language feature that solves one or more problems
Projects
None yet
Development

No branches or pull requests

1 participant