-
Notifications
You must be signed in to change notification settings - Fork 213
Stronger static typing for implicit casts and generics #796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Thanks for the writeup! Some comments:
The upcoming Null Safety release will include this restriction (except when the supertype is
We hope to ship definition site variance in 2020 (most of the implementation was already completed by my fall intern). This allows you to opt your own classes into sound invariance as you describe, but does not address the fact that legacy classes like We have discussed shipping "use site invariance" to provide a way to soundly interact with legacy covariance classes but have not committed to shipping this. Combined with type aliases, this would give you most of what you want here, I think.
There is ongoing discussion about whether to support this in some form. |
First: Yes, we'd like to give developers better support for static type safety! ;-) Just a drive-by comment:
This rule also exists for the designs of variance that we've considered, and it is actually an error even in Java to use a wildcard as a type argument in an invocation of a constructor: public class N000<X> {
public static void main(String[] args) {
Object o = new N000<? extends Object>(); // 'error: unexpected type'
}
} In general, the dynamic type of an instance |
I'd like to ask if this proposal mitigate the following thing and if not should I create a separate issue? Let's take a look at the following code:
In this made up example you can see that actual type would be As a real life example one can take a look at the following flutter code:
Without changing |
Well, that might depend on what exactly is meant by safe. Because of the way Dart's implicit interfaces (meaning the implements keyword) work, up-casting isn't always safe. For example, a function or static method that relies on a private field that the implementing sub-class doesn't have results in a runtime error. Or if the implementing sub-class by chance does have the required private field, but uses it for a completely other purpose, then it is also not safe. That has been the single biggest footgun of Dart so far. |
@ayporos, I think the way Dart handles it now is better, even if it's unintuitive. It reads through your code line by line, and at each point its decisions make sense. I think it would be worse if changing a line elsewhere (your In general, there are perfectly valid reasons to have a list of a subtype and not allow the supertype. This is a bit of a long example, but I hope it demonstrates the point: import "dart:math" as math;
abstract class Shape {
double get area;
}
class Square extends Shape {
double sideLength;
Square(this.sideLength);
@override
double get area => sideLength * sideLength;
/// Only applies to squares.
double get diagonalLength => sideLength * math.pow(2, 1/2);
}
class Circle extends Shape {
double radius;
Circle(this.radius);
@override
double get area => math.pi * math.pow(radius, 2);
/// Only applies to circles.
double get diameter => 2 * radius;
}
List<Shape> getSquares() {
var result = [Square(1), Square(2)]; // inferred as List<Square>
for (final Square square in result) {
print(square.diagonalLength); // can't do this with a Circle!
}
// This is the error you're talking about.
//
// If Dart looked at this and then typed result as List<Shape>, the for
// loop above wouldn't work. So it makes this the error instead of going back.
result.add(Circle(3));
return result;
}
void main() {
List<Shape> shapes = getSquares();
for (final Shape shape in shapes) {
print(shape.area); // this is okay
}
} |
@Levi-Lesches I think there might be misunderstanding. But allow me to start from the beginning.
Now to your example. Maybe I didn't make it clear but my problem is not that type inference of a line Using your example I don't care what is going on inside
Point is that signature (API) is saying that by calling
|
Right, so this is a common difficulty across I believe all OOP languages, not just Dart. There's a difference between an object's reference type (or static type), and its object type (or runtime type). The static type is the one you provide to Dart. In general, as long as the above condition is true, its always safe to use methods that belong to the static type, but not always those that belong to the object type. For example: void main() {
Shape myShape = getSquare(); // static: Shape, runtime: Square
print(shape.area); // safe
print(shape.diagonalLength); // unsafe, even though it really is a Square. Dart can't tell
} With generics, you have a new problem: this compile-time and runtime relationship is not always safe. The shapes example fits, but a much simpler one would be: void main() {
List<num> numbers = <int>[1, 2, 3]; // statically List<num>
print(numbers.runtimeType); // runtime: List<int>
numbers.add(4); // okay, since we're adding an int
numbers.add(5.5); // error, since this is a double
} A |
@Levi-Lesches sorry I have no time to read you answer in full but
is not true.
EDIT: now I think I'm having a Déjà vu, most likely I've created a separate thread somewhere here and got an answer (don't remember which one) so if you're interesting you can try to find it. |
Dart is somewhat unusual in having covariant (runtime checked) generics. Java has them for arrays only, and Eiffel had them, but most modern object oriented languages (Kotlin, Scala, Java generics (except arrays), C#, etc) use statically sound generics. See here for more exposition. |
What I was trying to say is that in my opinion Dart is not doing enough in preventing the worst error possible, the runtime one.
Try to replace for example |
Right, so variance (#524) sounds like what you're looking for. Dart will still infer the list as |
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>
toList<Object>
, as well as fromList<Object>
toList<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:The
futures
parameter has the typeIterable<Future<T>>
. If we prohibit casts from, for example,Future<int>
andFuture<String>
toFuture<Object>
, a data structure of typeIterable<Object>
can contain both aFuture<int>
and aFuture<String>
, but anIterable<Future<Object>>
cannot. As such, there is no way to pass futures of both types intoFuture.wait()
with its current type signature (unless of course the iterable is anIterable<Future<dynamic>>
). This restriction breaks the many call sites ofFuture.wait()
.As a remedy, I propose adding a feature similar to Java's wildcards, such that
Future.wait()
can be refactored as:At call sites, lists of the form
[Future.value(Foo()), Future.value(Bar())]
have deduced typeList<Future<? extends Object>>
, and passing such a list into this newFuture.wait()
causesT
to be inferred correctly asObject
.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:
The
swap
function is clearly sound under the assumption thatT
is a specific type. However, ifT
were allowed to be a wildcard, it is unsound. For example, ifpair
isPair<? extends Rodent>
, the runtime type might bePair<Mouse>
. If we assume within the function thatT
isRodent
, that allowstemp
to be properly initialized, but then later, the fieldpair.second
, which might be of typeMouse
, cannot soundly accept an assignment from an arbitraryRodent
.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.
The text was updated successfully, but these errors were encountered: