dart-lang / dart-lang/language

Allow factory constructors to specify a return type

Open
#4,153 4 comments 10 reactions 0 assignees View on GitHub
brevity enhanced-const feature
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

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:

```dart
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):

```dart
abstract class A {
const A._();
const factory B A() = B;
}

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

void main() {
A()..i = 1;
}
```

It may seem contradictory to wish to have the interface of `B` in an expression that looks like `A()` 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:

```dart
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:

```dart
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:

```dart
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:

```dart
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.

Contributor guide

Open the contributing guide

Research direction

Start with the redirecting and non-redirecting factory constructor examples in the issue, including generic subtype returns and `.identifier` invocation. Trace the related language-design discussion around constructor return types and determine the accepted syntax, subtype and type-argument constraints, and constant-constructor behavior. Done means the proposal has resolved these alternatives and precisely specifies the resulting behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.