dart-lang / dart-lang/language

Allow constant factory constructors to return a (potentially) constant value.

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

Description

It's currently not possible to have a `const` constructor which returns the same constant value each time.

Dart has generative constructors and factory constructors, redirecting and non-redirecting constructors, and constant and non-constant constructors. Of these eight possible combinations, only one is not allowed: The non-redirecting constant factory constructor.

That combination is what would be required to have a constant constructor which returns the same instance every time, even if called with `new`.
Constant generative constructors create a new object if invoked with `new`.
Forwarding constant factory constructors always end up invoking constant generative constructors in the same way, so if invoked with `new` it creates a new instance, just possibly of a subclass.

## Constant non-redirecting-const-factory return value.

I propose to allow the syntax:
```dart
const factory ConstructorName(args) => constantExpression;
```
for a non-redirecting constant factory constructor. When invoked it returns the value of `constantExpression`, which must be a subtype of the surrounding class (and if that class is generic, of every instantiation, which means implementing the interface with `Never` as type arguments).

Generally, constant constructors cannot have bodies, and non-redirecting factory constructors must have bodies, which is why the combination wasn't allowed.

However, if the body is a single constant expression, then the arguments against having bodies (running user code at compile time, possibly with side effects) do not apply. There is no semantical problem with allowing such a constructor.

The need for this constructor came up when refactoring an existing API that had a const constructor, and wanting it to return the same singleton instance every time. That was not possible in the current language. If all invocations of that constructor was with `const`, it would work, but it can also be invoked as `new`, and that would break the singleton pattern.

With this feature, the following pattern is then possible:
```dart
final class Abstraction {
static const _instance = const Abstraction._();
const Abstraction._();
const factory Abstraction() => _instance;
// members ...
}
```
and all invocations of `Abstraction()`, even using `new`, will provide the same canonical object. The class code can assume the identity of the object, and not have to defensively check if someone created a `new` instance, even if they were told not to.

## Potentially constant non-redirecting-const-factory return value
*Further*, we can even allow a *potentially constant expression* as the body, which can depend on the constructor parameters, the same way we allow such expressions in initializer list initializer expression, or in forwarding generative constructor arguments.
```dart
sealed class Formatter {
static const _prettyFormatter = PrettyFormatter();
static const _denseFormatter = DenseFormatter();
const Formatter._();
// Potentially constant expression.
const factory Formatter({bool pretty = false}) => pretty ? _prettyFormatter : _denseFormatter;
// Shared members
}
final class PrettyFormatter extends Formatter {
const PrettyFormatter() : super._();
// Pretty methods
}
final class DenseFormatter extends Formatter {
const DenseFormatter() : super._();
// Dense methods
}
```
I believe that could be *really useful* too, but it's not a requirement, and allowing only constant expressions is valuable by itself.

## Workaround (to become possible!)

A workaround/hack using _extension types_ can provide this functionality, because extension type constructors can *create the representation object* in an initializer expression/forwarding parameter, where we do allow potentially constant expressions, and the representation object *is* the object being created.
```dart
sealed class Formatter {
static const _prettyFormatter = PrettyFormatter._();
static const _denseFormatter = DenseFormatter._();
const factory Formatter({bool pretty}) = _FormatterChoice;
const Formatter._();
// Shared members
}

final class PrettyFormatter extends Formatter {
const PrettyFormatter._() : super._();
// Pretty methods
}

final class DenseFormatter extends Formatter {
const DenseFormatter._() : super._();
// Dense methods
}

// Subtype of `Formatter`, so can be returned by the `Formatter` constructor.
extension type const _FormatterChoice._(Formatter _) implements Formatter {
const _FormatterChoice({bool pretty = false})
: this._(
pretty ? Formatter._prettyFormatter : Formatter._denseFormatter);
}

void main() {
// true
print(identical(const Formatter(pretty: true), new Formatter(pretty: true)));
// true
print(!identical(
const Formatter(pretty: true), const Formatter(pretty: false)));
// Pretty!
print(switch (const Formatter(pretty: true)) { // Is exhaustive.
PrettyFormatter _ => "Pretty!",
DenseFormatter _ => "Dense!",
});
}
```
This code should (based on the current extension-type proposal) allow `const Formatter(pretty: true)` to choose one of two actual constant formatters, both if invoked with `new` or with `const`. The extension type is immediately forgotten when returned by the `Formatter` constructor.
I tested, this works with the current implementation of extension types.

If the "choose one of two constants" (or "always one constant") functionality is already (or soon) _technically possible_ using extension types, then we should consider making it available _more directly_, to not encourage writing extra code only as
a workaround.

Contributor guide

Open the contributing guide

Research direction

Start by reviewing issue 3356, including the proposed constant factory syntax, singleton example, potentially constant variant, and extension-type workaround. No source files or tests are named; done would require a language-design decision and corresponding specification work defining whether the feature and its constraints are accepted.

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
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.