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) 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<String>
to List<Object>
, as well as from List<Object>
to List<String>
. 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:
static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
{bool eagerError: false, void cleanUp(T successValue)}) {
/* ...implementation... */
}
The futures
parameter has the type Iterable<Future<T>>
. If we prohibit casts from, for example, Future<int>
and Future<String>
to Future<Object>
, a data structure of type Iterable<Object>
can contain both a Future<int>
and a Future<String>
, but an Iterable<Future<Object>>
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<Future<dynamic>>
). This restriction breaks the many call sites of Future.wait()
.
As a remedy, I propose adding a feature similar to Java's wildcards, such that Future.wait()
can be refactored as:
static Future<List<T>> wait<T>(Iterable<Future<? extends T>> 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<Future<? extends Object>>
, 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:
class Rodent {}
class Mouse extends Rodent {}
class Pair<T> {
Pair(this.first, this.second);
T first;
T second;
}
void swap<T>(Pair<T> 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<? extends Rodent>
, the runtime type might be Pair<Mouse>
. 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.