dart-lang / dart-lang/language

[extension types] type-annotation-driven resolution?

Open
#3,373 7 comments 0 reactions 0 assignees View on GitHub
Dominant language
TeX
Stars
2.9k
Forks
239
Avg merge
2d 18h
Merged PRs (30d)
14

Description

The mental model that I have of type annotations in Dart is that they are only (_mostly?_) a tool for helping the type-checker type-check in cases where it doesn't have enough information to infer the types by itself, and would have to "fail" otherwise.

If a program A doesn't compile successfully, because it doesn't type-check, type annotations help with making A compile successfully.

Extension types appear to make it so that we can have a program B that compiles successfully, but we can use type annotations to change the runtime behavior of B. That is, changing the type annotation of a variable that is assigned a value that holds an extension type can change the meaning of a program (_not just in ways that makes it succeed or fail compilation_):

```dart
void main() {
final Foo i = Foo(2);
print(i.toDouble()); // 0
final int j = i; // We change the type annotation of i to int.
print(j.toDouble()); // 2
final k = i; // We remove the type annotation of i.
print(k.toDouble()); // 0
}

extension type Foo(int i) implements int {
double toDouble() {
return 0.0;
}
}
```

This is **very** surprising to me, because I am consciously relying on this not to be the case.

I'm used to being able to freely add/remove type annotations because of the assumption that this is an invariant program transformation with respect to the semantics of my program. (_something like, if it type-checks, it works, and adding more type annotations can help with making things work, but not change how things work._)

However, with extension types, this property will seemingly be lost. And any added/deleted type annotation would have to be carefully inspected to ensure that it doesn't change the meaning of my program beyond just a binary it compiles/it doesn't.

---

This also introduces confusing situations like the following:

Notice how `print(i.toDouble()` outputs a `0`, but if we print via `Bar`, we output a `2`, same with `k`:

```dart
void main() {
final Foo i = Foo(2);
print(i.toDouble()); // 0
final int j = i;
print(j.toDouble()); // 2
final k = i;
print(k.toDouble()); // 0
Bar(i).output(); // contains i but outputs a 2
Bar(j).output(); // contains j but outputs a 2
Bar(k).output(); // contains k but output a 2
}

extension type Foo(int i) implements int {
double toDouble() {
return 0.0;
}
}

class Bar {
final int i;

Bar(this.i);

void output() {
print(i.toDouble());
}
}
```

Of course, this probably is not surprising to the language team, but I wanted to point out that this is surprising to me as a user of Dart, and I think that it would be very difficult to explain this behavior to somebody that is new to programming or even just new to Dart.

---

Erik pointed out an important use-case for the ability to implement supertypes here (https://github.com/dart-lang/language/issues/3365#issuecomment-1735582023). While I do see the value in that, I've shared an opinion here (https://github.com/dart-lang/language/issues/3365#issuecomment-1735640181) that I don't think it would sufficiently solve the use-case of migrating from an old interface to a new one because a level of indirection would negate the safety guarantees that the current design would make possible.

To summarize, I don't think that being able to implement supertypes of the representation type "carries its weight" if it introduces such, in my opinion, "confusing" behaviors.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.