dart-lang / dart-lang/language
Allow constant factory constructors to return a (potentially) constant value.
- 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
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