dart-lang / dart-lang/language
Type parameters with default value for classes
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
This is a possible solution for #283 and other similar issues.
This is the first time writing a slightly formal proposal, so please be patient and let's work together to improve this.
I may not have considered potential issues, and my language is certainly not as precise as it should be, but I think the idea can be understood.
## Overview
This proposal suggests that we allow for a type parameter `T` on a class to have a default value.
With this proposal, we could do the following:
```dart
// The default value for `T` in `Foo` is `int`.
class Foo {
Foo({
this.bar = 42,
T baz = 0,
T qux = 'a', // Error: The default value for a variable of the generic type 'T' should have a type 'int'.
});
final T bar;
}
class Quuz extends Foo {}
typedef Quux = Foo;
void main() {
var fooDefault = Foo(); // Inferred as Foo instead of Foo
print(fooDefault.bar); // 42
var fooStringWrong = Foo(); // Error: The named parameter 'bar' is required, but there's no corresponding argument.
var fooStringRight = Foo(bar: '42', baz: '0', bar: 'a');
var quuz = Quuz();
print(quuz.bar); // 42
var quux = Quux(bar: '42', baz: '0', bar: 'a');
}
```
## Motivations
### Adding or removing type parameters from a class is inconvenient (https://github.com/dart-lang/language/issues/283)
Consider the following cases:
#### Case 1: Changing the number of type paramters of a class that already has type parameters
```dart
// Library
class Foo {}
// Client
class Bar extends Foo {}
```
Then, adding a new type parameter to `Foo` is a breaking change:
```dart
// Library
class Foo {}
// Client
class Bar extends Foo {} // The type 'Foo' is declared with 2 type parameters, but 1 type arguments were given.
```
#### Case 2: If the client has `strict-raw-types` enabled, adding a type parameter to a class that has no type parameter
```dart
// Library
class Foo {}
// Client
class Bar extends Foo {}
```
Then, adding a new type parameter to `Foo` is a breaking change:
```dart
// Library
class Foo {}
// Client
class Bar extends Foo {} // The generic type 'Foo' should have explicit type arguments but doesn't.
```
#### Known workarounds
##### Using `typedef`
For some cases, using `typedef` may be a valid workaround.
Consider the following case.
```dart
// Library
class Foo {}
// Client
class Bar extends Foo {}
```
Instead of simply adding a new type parameter to `Foo`, we provide another class and `Foo` as a `typedef`.
```dart
// Library
class NewFoo {}
typedef Foo = NewFoo; // Here, `int` is the "default" value for `U`
// Client
class Bar extends Foo {} // The client does not break
```
However, this is not always desirable, because if we want `U` to be used by the client, it will have to refer to the new class:
```dart
// Library
class NewFoo {}
typedef Foo = NewFoo; // Here, `int` is the "default" value for `U`
// Client
class Bar extends Foo {}
class Baz extends NewFoo {} // Baz can't simply extend `Foo`, because `U` is only available in `NewFoo`.
```
Having a default value, in this case, wouldn't be less breaking. It would still be breaking for `Baz` to support the `U` parameter. However, it would be done without the need of two different classes.
### Allowing for default values in constructors where the field type is a generic type
Currently, the following is an error:
```dart
class Foo {
Foo([this.bar = 42]); // A value of type 'int' can't be assigned to a variable of type 'T'.
final T bar;
}
```
By having a default type parameter value, we can accept default values in the constructor where `T` is expected, as long as the type has type `U` and `U <: D`, being `D` the default type for `T`.
### Reduce "clutter" for common cases
This is more than nothing a way of making some codes terser.
For intance, consider the following library code:
```dart
// --- Library.
class A {}
abstract interface class Tag {}
final class DefaultTag implements Tag {}
```
Now, we don't know how the clients are going to use library, but we know that the common case is to use `DefaultTag` as parameter. The client can introduce their own `Tag`s, but it's an exceptional case that's not commonly used.
By providing `DefaultTag` as the default value for `T`, the majority of the clients can extend from the raw type `A`, while the exceptional cases can specify their custom `Tag`.
## Syntax
Considering that we are dealing exclusively with classes[^1], the grammar for class declaration would be changed in the following way:
[^1]: I think it is possible to extend this to non-classes too, like regular functions methods, but it's out of the scope of this proposal. If we do, we don't have to split `typeParameter` into `classTypeParameter`.
```diff
+⟨classTypeParameter⟩ ::= ⟨metadata⟩ ⟨identifier⟩ (extends ⟨typeNotVoid⟩)? (default ⟨type⟩)?
+⟨classTypeParameters⟩ ::= ‘<’ ⟨classTypeParameter⟩ (‘,’ ⟨classTypeParameter⟩)* ‘>’
-⟨classDeclaration⟩ ::= abstract? class ⟨typeIdentifier⟩ ⟨typeParameters⟩? ⟨superclass⟩? ⟨interfaces⟩?
+⟨classDeclaration⟩ ::= abstract? class ⟨typeIdentifier⟩ ⟨classTypeParameters⟩? ⟨superclass⟩? ⟨interfaces⟩?
‘{’ (⟨metadata⟩ ⟨classMemberDeclaration⟩)* ‘}’
| abstract? class ⟨mixinApplicationClass⟩
-⟨typeAlias⟩ ::= typedef ⟨typeIdentifier⟩ ⟨typeParameters⟩? ‘=’ ⟨type⟩ ‘;’
+⟨typeAlias⟩ ::= typedef ⟨typeIdentifier⟩ ⟨classTypeParameters⟩? ‘=’ ⟨type⟩ ‘;’
| typedef ⟨functionTypeAlias⟩
```
This would allow us to specify the type parameter default value with the following syntax:
```dart
class Foo {}
class Bar {}
typedef Baz = Foo;
typedef Quz = Bar;
```
### Alternative syntax
#### Using `=` instead of `default`
For default values in a parameter, `=` is used.
```dart
void foo([int bar = 42]) {}
```
We could use `=` also for default type parameter values.
```dart
class Foo {}
class Bar {}
// OR
class Bar {}
```
The syntax is terser.
However, one[^2] could argue that it's syntatically confusing when used with `extends`.
[^2]: https://github.com/dart-lang/language/issues/283#issuecomment-839603127
In the first case, `T extends num = int` may give the idea that we are equalling `num` to `int`. In the second case, `T = int extends num` may give the idea that `int` extends `num` (which is not entirely false, but in the context we want to mean that `T` extends num).
There are other shorter keywords that could be used, but I don't think any of them is semantically clearer than `default`.
#### Using parenthesis instead of `default`
[As suggested by @hydro63](#issuecomment-2334227922), an alternative would be to use parenthesis, in the following way:
```dart
class Foo {}
class Bar {}
```
This approach has the following pros:
- It's as terse as using `=`;
- It's not as ambiguous as `=` when used with `extends`.
The con, however, in my opinion, is that it does not conveys the semantics as good as `default` or `=`.
Someone who is just starting with Dart may understand what `T default int` means, but will hardly understand what `T(int)` means without reading the documentation.
## Semantics
* Let `T` be a type parameter of a class `C` with default value `V`:
* It's a compile-time error if `T` has a bound `U` and `V <: U`[^3] does not hold;
* For all constructors `c` in `C`, any parameter of type `T` in `c` can have a default value `v` iff `v` is constant and `v` has a static type `V2` such that `V2 <: V`[^3];
* When `C` is used as a raw type:
* If `T` has no bound, the _instantiation to bound algotithm_ should infer `T` to be `V` instead of `dynamic`;
* If `T` has a bound `U`, the _instantiation to bound algotithm_ should infer `T` to be `V` instead of `U`.
[^3]: These semantics may be changed if we have statically checked declaration-site variance. For instance, if the type parameter `T` is contravariant with a default value `U`, we may want to guarantee that `T <: U`.
## Other questions
### Should default type parameter value be inferred from constructor?
Consider this same code as before:
```dart
class Foo {
Foo([this.bar = 42]);
final T bar;
}
```
With the proposed changes above, it still wouldn't work, unless we specified `T default int`.
We can infer `default int` from the default parameter of the constructor.
If we have more than one parameter, like:
```dart
class Foo {
Foo([this.bar = 42, this.baz = 42.0]);
final T bar;
}
```
Then we could apply the LUB algorithm. In this case, it would be inferred as `T default num`.
My personal opinion is that we shouldn't do this, and I would rather prefer for a default value for the type parameter to be always explicitly stated.
Contributor guide
Research direction
Start with the Overview, Syntax, and Semantics sections, then review the linked issue #283 and the alternative-syntax discussion. Done means the proposal's grammar and semantics have an agreed resolution, including how defaults interact with bounds, raw types, constructors, and typedefs.
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
- 30/100