dart-lang / dart-lang/language
Strict bounds
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
This is a proposal to enable a new relationship in the declaration of a formal type parameter: It should be possible to declare that the corresponding actual type argument must be a proper subtype of the bound, it is not enough to be equal to the bound.
```dart
sealed class S {}
class A extends S {}
class B extends S {}
class C extends S {}
void f(X x) {}
```
The type parameter `X` is treated the same way as `X extends S`, except that it is an error for `X` to be equal to `S` (that is, `X <: S` must be true, as usual, but `S <: X` must be false).
This mechanism can play a role which is somewhat similar to that of an abstract class: It is able to provide a certain amount of structure, but it cannot be used directly.
The abstract class describes properties of all subtypes, but we can't create an instance of the abstract class itself. The strictly bounded type parameter will constrain the possible actual type arguments as usual, but the type which is used to specify the bound itself cannot be used. You could say that the strict bound turns the bound into an "abstract type argument".
So how could this be useful?
In general, it allows us to denote a set of types that we often cannot enumerate. In the example above `S` is a sealed class, which means that it _is_ possible to enumerate the immediate subtypes of `S`, but with any type which isn't sealed we cannot know that we have included them all. Also, it might be convenient to use `strictly extends` even with a sealed type, because it is more maintainable.
A strictly bounded type parameter could be used with bound `Object?` to ensure that a type argument can be any type except a top type, which is something that we can't otherwise express. In particular, this means that the actual type argument cannot be `dynamic`, but it _can_ be another nullable type (and it can even be `Null`). This is not possible if we just specify the bound to be `Object` (which is currently the quasi-standard way to express that a type argument can't be `dynamic`).
It can also be used with `Object?` in order to ensure that it is a compile-time error to rely on the greatest closure (that is, the default approach taken when type inference has no information).
```dart
class A {}
void main() {
var a = A(); // Compile-time error, because of `strictly`.
print(a.runtimeType); // Without `strictly`: 'A'.
var a2 = A(); // No problem!
}
```
A strict bound could be used with `Object` in order to avoid a subtle type like `FutureOr` (see https://github.com/dart-lang/sdk/issues/54311 for some background info about why we might want to avoid such types).
```dart
FutureOr f() ...
void main() {
FutureOr x = f(); // Used to be subtle; now an error.
...
}
```
A strict bound could be used with a sealed type in order to specify a finite, statically known set of possible type arguments to a generic entity, and at the same time preventing the sealed type itself from abstracting over this set of types as a whole (is the "abstract type argument" usage: all the subtypes are "concrete as type arguments" and can be used as actual type arguments, but the sealed type itself is "abstract as a type argument" and cannot be used directly):
```dart
class Unit {}
class Meter implements Unit {}
class Foot implements Unit {}
extension type Length(double value) {
Length operator +(Length other) => Length(value + other.value);
}
Length add(Length l1, Length l2) {
return l1 + l2;
}
void main() {
var l1 = Length(1.0);
var l2 = Length(3.5);
var l3 = Length(-1.7178); // Compile-time error, "you must choose a concrete unit".
var l4 = l1 + l1; // OK, is `Length`.
var l5 = add(l1, l1); // OK, addition in `add` is polymorphic.
var l6 = add(l1, l2); // Compile-time error.
}
```
Without `strictly` we cannot ensure that the units are respected:
```dart
void main() {
var l1 = Length(1.0);
var l2 = Length(3.5);
var l6 = add(l1, l2); // No error, but should be rejected. Inferred as `add(l1, l2)`.
}
```
Contributor guide
Research direction
Start by reviewing the proposed `strictly extends` semantics and the examples in this issue. No implementation files, entry points, or tests are identified in the payload. Done would require an agreed language design, specification changes, and corresponding analysis and diagnostic coverage.
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
- 25/100