dart-lang / dart-lang/language

Make it safer to "enter" a (plain) view or an extension type

Open
#1,665 5 comments 1 reaction 0 assignees View on GitHub
extension-types-later
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.