Skip to content

Instantly share code, notes, and snippets.

@munificent
Created July 12, 2018 17:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save munificent/6296132ac1d5179ebe9f98709bcea37b to your computer and use it in GitHub Desktop.
Save munificent/6296132ac1d5179ebe9f98709bcea37b to your computer and use it in GitHub Desktop.
Function-typed fields and covariant generics
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