dart-lang / dart-lang/language
Make it safer to "enter" a (plain) view or an extension type
- Dominant language
- TeX
- Stars
- 2.9k
- Forks
- 239
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 14
Description
This issue proposes a rather mild increase in the protection of views and extension types: We can require that a constructor is called in order to turn an expression of the on-type into an expression of the view/extension type, and still enable higher order cases (such as lists and functions) to allow assignments without a cast.
This proposal about [views](https://github.com/dart-lang/language/pull/1617) and this proposal about [extension types](https://github.com/dart-lang/language/blob/master/working/1426-extension-types/feature-specification.md) allow us to make the on-type a subtype of the view or extension type (I'll mention views here, but `view` could be replaced by `extension type` everywhere):
```dart
view IdNumber on int {} // No members declared, this topic is not about them.
view ProductNumber on int {}
void main() {
IdNumber id = 24779; // OK, this is an upcast.
int i = id; // Error, this is a downcast.
i = id as int; // OK.
ProductNumber pn = id; // Error, these types are unrelated.
}
```
This ensures that different views on the same kind of object are not mutually assignable, so we get an error if we assign `id` to `pn`. This is quite useful in the situation where we want to use a given representation (perhaps a very lightweight one like `int`) in several different ways that may need to be kept separate according to the application logic.
However, this subtype based approach provides no checks on the _introduction_ of new values of the given view type, we're always allowed to assign a given `int` to any of those `...Number` view types:
```dart
view AgeInYears on int {}
view WeightInPounds on int {}
class Person {
String name;
AgeInYears age;
WeightInPounds weight;
Person(this.name, this.age, this.weight);
}
void main() {
Person('John Doe', 239, 74); // Oops, should have been `74, 239`!
}
```
There is no compile-time error in `main` above, and no run-time error, it is a logical error because those two numbers were _intended_ to be passed in the opposite order. So the program has a bug even though it might not crash.
In order to improve on the correctness support at the point where instances typed as a view type are obtained, we could add a simple rule to the above mentioned proposals:
**Assignability** is extended by an extra check: A type _T_ is assignable to a type _S_ if
- _T_ is `dynamic`, or
- _T <: S_ and _S_ is not a view type with a conversion constructor, or
- _T <: S_ and _S <: T_.
A _conversion constructor_ is a view constructor with the same name as the view that takes a single, mandatory, positional argument whose type is the on-type of the view.
This means that if we declare a conversion constructor in a view then it is required that we call this constructor in the case where we wish to use an expression whose type is the on-type where the view type is expected.
For example:
```dart
view AgeInYears on int {
factory AgeInYears(int value) {
assert(value < 201);
return value;
}
}
view WeightInPounds on int {
factory WeightInPounds(int value) => value;
}
class Person {
String name;
AgeInYears age;
WeightInPounds weight;
Person(this.name, this.age, this.weight);
}
void main() {
Person('John Doe', WeightInPounds(239), AgeInYears(74)); // Compile-time error.
}
```
Note that we can of course use normal abstraction on top of the conversion constructor, if we wish to make things look a bit differently:
```dart
extension on int {
AgeInYears get ageInYears => AgeInYears(this);
WeightInPounds get weightInPounds => WeightInPounds(this);
}
...
void main() {
...
Person('John Doe', 74.ageInYears, 239.weightInPounds); // OK.
}
```
The reason why it could be useful to change assignability and keep the subtype relationship from the on-type to the view type is that this allows us to transparently enable adoption of the view on composite entities. For example, we can create a `List` in some context (where `IdNumber` is not available), and then we can work with that list (there's no need to copy it) as a `List`:
```dart
view IdNumber on int {
factory IdNumber(int i) => i;
}
void main() {
List xs = [24779, 24780, 24781];
List ids = xs; // OK, this is still an upcast.
}
```
**PS**: In some situations we might wish to take away the assignability in the higher-order cases, too. In order to achieve that we could use something like a _closed view_ (mentioned in the [view proposal](https://github.com/dart-lang/language/pull/1617)), so we can make this distinction a choice that developers can make. Example:
```dart
closed view IdNumber on int { // `IdNumber` and `int` are statically unrelated types.
factory IdNumber(int i) => i;
}
void main() {
List xs = [24779, 24780, 24781];
List ids = xs; // Error, `List` and `List` are unrelated.
ids = xs.map((x) => IdNumber(x)).toList(); // The safe way to do it: Create a new list.
ids = xs as List; // This will actually succeed at run-time.
}
```
The cast `as List` will succeed at run time (assuming the current proposal), because every view type is reified as the corresponding on-type at run time. However, it is statically unsafe in the sense that we could as well have made `xs` a `Set` (or a `String`, or anything at all), and in that case `xs as List` would throw at run time even though there is no compile-time error. This is a major reason why we might want to offer the mild protection described in this issue.
Contributor guide
Research direction
Start by reading the linked views proposal and extension-type feature specification, then compare their assignability rules with the conversion-constructor proposal here. The issue names no repository files, tests, or implementation entry point; done would require an agreed language-design change and corresponding specification work.
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