> A mutable reference type is covariant on read, and contra on write.
No it isn't. The type is covariant because a reference is a subtype of all parent types. The the function read must have it's argument invariant because it both takes and returns an instance of the type. I think you're confusing the variance of types for the variance of instances of those types. Read is effectively a Function(t: type, Function(ref t, t)). If it was covariant as you suggest, we would have a serious problem. Consider that (read Child) works by making a memory read for the first (sizeof Child) bytes of it's argument. If read were covariant, then that would imply you could call (read Child) on a Parent type and get a Parent back, but that won't work because (sizeof Child) can be less than (sizeof Parent). Read simply appears covariant because it's generic. (read Child) ≠ (read Parent), but you can get (read Parent). It also appears contravariant because you can get (read Grandchild).
Scala doesn't simplify anything, that's just how variance works.
Even in your own description, it is clear that with regards to _correctness_, the variance model bifurcates between the read and the write method.
The discussion about the type sizes is a red herring. If the type system in question makes two types of differing sizes not able to be subtypes of each other, then calling these things "Child" and "Parent" is just a labeling confusion on types unrelated by a subtyping relationship. The discussion doesn't apply at all to that case.
The variance is a property of the algorithm with respect to the type. A piece of code that accepts a reference to some type A and only ever reads from it can correctly accept a reference to a subtype of A.
A piece of code that accepts a reference to a type A and only ever writes to it can correctly accept a reference to a supertype of A.
In an OO language, an instance method is covariant whenever the subject type occurs in return position (read analogue), and it's contravariant whenever the subject type occurs in a parameter position (write analogue). On instance methods where both are present, you naturally reduce to the intersection of those two sets, which causes them to be annotated as invariant.
> A piece of code that accepts a reference to some type A and only ever reads from it can correctly accept a reference to a subtype of A.
The same is true of a piece of code that writes through the reference or returns it. That's how sub-typing works.
> A piece of code that accepts a reference to a type A and only ever writes to it can correctly accept a reference to a supertype of A.
Have you ever programmed in a language with subtyping? Let me show you an example from Java (a popular object oriented programming language).
class Parent {}
class Child {
int x;
}
class Example {
static void writeToChild(Child c) {
c.x = 20;
}
static void main() {
writeToChild(new Parent());
}
}
This code snippet doesn't compile, but suppose the compiler allowed us to do so, do you think it could work? No. The function writeToChild cannot accept a reference to the supertype even though it only writes through the reference.
I've seen a lot of people in this comment section talking about read and write which I find really odd. They have nothing to do with variance. The contravariant property is a property of function values and their parameters. It is entirely unrelated to the body of the function. A language without higher order functions will actually never have a contravariant type within it. This is why many popular OOP languages do not have them.
> The same is true of a piece of code that writes through the reference or returns it. That's how sub-typing works.
But it is not true that it is correctly typed with respect to a a supertype of A (it is not valid to call the code with a reference to a supertype of A).
Code that only writes through the reference is correctly typed with respect to a super-type of A (it is valid to call the code with a reference to a supertype of A).
> Have you ever programmed in a language with subtyping?
Sigh, keep that snark for the twitter battles. I don't care enough about this to get snippy about it or to deal with folks who do.
> Code that only writes through the reference is correctly typed with respect to a super-type of A (it is valid to call the code with a reference to a supertype of A).
I'm not trying to be snarky, I genuinely want to know what you think of that code snippet I showed you. You claim it should work, but anyone with an understanding of programming would tell you it shouldn't. You can't write to a member variable that doesn't exist. Have you encountered inheritance before? Do you know what a “super-type” is? The mistake you're making here is very basic and I should like to know your level of experience.
No it isn't. The type is covariant because a reference is a subtype of all parent types. The the function read must have it's argument invariant because it both takes and returns an instance of the type. I think you're confusing the variance of types for the variance of instances of those types. Read is effectively a Function(t: type, Function(ref t, t)). If it was covariant as you suggest, we would have a serious problem. Consider that (read Child) works by making a memory read for the first (sizeof Child) bytes of it's argument. If read were covariant, then that would imply you could call (read Child) on a Parent type and get a Parent back, but that won't work because (sizeof Child) can be less than (sizeof Parent). Read simply appears covariant because it's generic. (read Child) ≠ (read Parent), but you can get (read Parent). It also appears contravariant because you can get (read Grandchild).
Scala doesn't simplify anything, that's just how variance works.