dart-lang / dart-lang/language

Stronger static typing for implicit casts and generics

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

Description

I am a Google engineer on the Assistant Infrastructure team, and there is an effort underway to migrate part of our infrastructure from C++ to Dart. I have been investigating Dart's type system for this purpose, and I stumbled upon some significant issues with the static type system. These issues are probably nothing new to the Dart Language team, but they are surprising to me and my team. I wrote a detailed doc (PDF: [Stronger static typing in Dart](https://github.com/dart-lang/language/files/4110793/Stronger.static.typing.in.Dart.pdf)) describing these issues and proposing some remedies. The primary audience for this doc was engineers within my own org, so it is somewhat pedantic with its discussion of static typing. I highly encourage you to read it, but I'll attempt to summarize its major recommendations here:

### Prohibit implicit casts from a supertype to a subtype
Implicit casts from a subtype to a supertype are always safe, but casts from a supertype to a subtype require a runtime check, and so they should be made explicit using the `as` keyword. Failure to make the cast explicit should be a static error.

### Prohibit casts between generic classes with different type arguments
Both statically _and at runtime_, it should be an error to cast from, for example, `List` to `List`, as well as from `List` to `List`. Such casts are inherently unsafe because of contravariant types among the method parameters in generic classes.

### Support type bounds as arguments to generic classes
If casts between generic classes with different specific type arguments are prohibited, it would prevent certain Dart idioms from working, even after refactoring, without abandoning static type safety entirely. For example, consider the method signature for `Future.wait()` in Dart's standard library:

```dart
static Future> wait(Iterable> futures,
{bool eagerError: false, void cleanUp(T successValue)}) {
/* ...implementation... */
}
```
The `futures` parameter has the type `Iterable>`. If we prohibit casts from, for example, `Future` and `Future` to `Future`, a data structure of type `Iterable` can contain both a `Future` and a `Future`, but an `Iterable>` cannot. As such, there is no way to pass futures of both types into `Future.wait()` with its current type signature (unless of course the iterable is an `Iterable>`). This restriction breaks the many call sites of `Future.wait()`.

As a remedy, I propose adding a feature similar to [Java's wildcards](https://docs.oracle.com/javase/tutorial/extra/generics/wildcards.html), such that `Future.wait()` can be refactored as:
```dart
static Future> wait(Iterable> futures,
{bool eagerError: false, void cleanUp(T successValue)}) {
/* ...implementation... */
}
```
At call sites, lists of the form `[Future.value(Foo()), Future.value(Bar())]` have deduced type `List>`, and passing such a list into this new `Future.wait()` causes `T` to be inferred correctly as `Object`.

Unlike Java's wildcards, I propose that it is only valid to use wildcards as type arguments for generic class _types_, not as type arguments for generic functions or constructors; types parametrized by a wildcard are fully abstract. The reason is that Dart's runtime system requires that variables have specific runtime types, and wildcards are not themselves types. For example, consider this generic function:

```dart
class Rodent {}
class Mouse extends Rodent {}

class Pair {
Pair(this.first, this.second);
T first;
T second;
}

void swap(Pair pair) {
T temp = pair.first;
pair.first = pair.second;
pair.second = temp;
}
```

The `swap` function is clearly sound under the assumption that `T` is a specific type. However, if `T` were allowed to be a wildcard, it is unsound. For example, if `pair` is `Pair`, the runtime type _might_ be `Pair`. If we assume within the function that `T` is `Rodent`, that allows `temp` to be properly initialized, but then later, the field `pair.second`, which might be of type `Mouse`, cannot soundly accept an assignment from an arbitrary `Rodent`.

The remainder of my attached document lays out some related features for static type inference that would be very helpful to minimize the efforts to refactor existing code to comply with my recommended prohibitions. It also presents an algorithm for determining the types of methods within a generic class, given its specific and/or bounded type arguments.

I thank you for considering these changes to the Dart language. Please let me know if you have any questions for me.

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.