Created
July 12, 2018 17:29
-
-
Save munificent/6296132ac1d5179ebe9f98709bcea37b to your computer and use it in GitHub Desktop.
Function-typed fields and covariant generics
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Foo<T> { | |
Function(T) callback; | |
T field; | |
add(T thing) { | |
T local = thing; | |
} | |
} | |
takeInt(int i) => print(i + 1); | |
takeObject(Object o) => print(o); | |
main() { | |
// This is fine: | |
Foo<int> fooOfInt = Foo<int>(takeInt); | |
// And inference does the same thing: | |
var alsoFooOfInt = Foo(takeInt); | |
// Accessing the callback is fine too: | |
fooOfInt.callback; // <-- No runtime cast here. | |
// Then you store a reference to Foo<int> in a variable of type Foo<Object>: | |
Foo<Object> fooOfObject = fooOfInt; | |
// This is statically unsound. Dart allows it because all it treats all | |
// generics as covariant, even ones that shouldn't be. | |
// To preserve soundness, it has to insert runtime checks when you use Foo in | |
// a covariant way. | |
// Like when calling a method: | |
fooOfObject.add("not an int"); | |
// The body of add() tries to store that string in a local variable whose | |
// type is int. Allowing that would be bad, so we insert a runtime cast in | |
// the body of `add()` to check that the parameter type matches its expected | |
// type. So the desugared version of `add()` looks something like: | |
// | |
// add(Object thing_) { | |
// T thing = thing_ as T; | |
// T local = thing; | |
// } | |
// | |
// We can push that check into the body off `add()` because we know that body | |
// is used for the declaration of `add()` itself, and only for Foo. We control | |
// it. | |
// We also insert runtime checks when using fields. Sometimes, reading a | |
// field is a statically sound covariant operation, so we don't need a | |
// runtime check for that. This is fine: | |
Object o = fooOfObject.field; | |
// But setting this field is not sound, so we do insert a runtime check here: | |
fooOfObject.field = "not an int"; | |
// In this case the check would fail. | |
// Whether or not setting or getting a field needs a runtime check depends on | |
// where in the field's type it mentions `T`. In the above case, the field's | |
// type is exactly `T` which means getting is safe and setting is not. `T` is | |
// occurring in a "covariant position". | |
// But when you have a field whose type happens to be a function and one of | |
// the function's parameters is `T`, now `T` is in a "contravariant position". | |
// That flips things around. Setting is now safe: | |
fooOfObject.callback = takeObject; | |
// Find, since the function is more permissive than Foo<int> requires. This | |
// will always be true, so we never need to check here. | |
// But getting is not: | |
Function(object) fn = fooOfObject.callback; | |
fn("not an int"); | |
// So, when you access this field, we have to insert a runtime check. | |
// We can't push the check into a parameter check in the function body, | |
// because we don't control the function that you're storing in that callback | |
// field. It could come from anywhere. We can't break other uses of the | |
// function. This should still work: | |
takeObject("not an int"); | |
// We also can't (easily) wrap the function when you store it because that | |
// messes with identity and makes it hard to know when to *unwrap* it. This | |
// caused major problems for the gradual typing folks. (Google "is gradual | |
// typing dead".) | |
// | |
// So we do the insert the check at the access site. But, again, remember | |
// that this isn't a problem with the function field itself, but with the | |
// use of the surrounding class. If Foo wasn't generic, or wasn't used in a | |
// covariant way, everything is fine. | |
// At this point, you may be wondering why the hell all generics are | |
// covariant in Dart. The answer for Dart 1 was that many covariant uses of | |
// generics are actually fine, so it's useful to permit them. Letting the | |
// user specify which are allowed and which are not is quite complex and the | |
// language was unsound anyway, so it just erred on the side of | |
// permissiveness. | |
// | |
// We didn't fix it for strong mode because we felt the migration would have | |
// been too difficult on top the already difficult migration to generic | |
// methods, etc. | |
// | |
// There is some desire to add support for controlling variance in a future | |
// version of Dart. With that, you could make this a static error: | |
Foo<Object> fooOfObject2 = fooOfInt; | |
// And once that's an error, you can't even get into the downstream situation | |
// where you need to worry about accessing `callback` in a covariant way. | |
} | |
// --- Stop here unless you want to nerd out. --- | |
// Contravariance actually *toggles* the direction of variance each time it | |
// nests, so a contravariant position inside a contravariant position becomes | |
// a covariant one. So if you have this weird thing: | |
class Bar<T> { | |
// Fields whose type is a function that takes a function that takes a `T`: | |
Function(Function(T)) nestedCallback; | |
} | |
main2() { | |
// Now that `T` is nested inside two levels of contravariances, so it flips | |
// back to being covariant. That means you need to do runtime checks when | |
// storing but not accessing. | |
takeTakeIntCallback(Function(int) callback) callback(1); | |
takeTakeObjectCallback(Function(Object) callback) callback("not an int"); | |
var barOfInt = Bar<int>(); | |
barOfInt.nestedCallback = takeTakeIntCallback; | |
var barOfObject = barOfInt; | |
// This is fine: | |
Function(Function(Object)) callback = barOfObject.nestedCallback; | |
callback(takeObject); | |
// But this needs to be checked: | |
barOfObject.nestedCallback = takeTakeObjectCallback; | |
// Fails because otherwise, you could do: | |
barOfInt.nestedCallback((int i) => i + 1); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment