5 ms·
Why? It's just a property of type transformation. Assuming Parent <- Child ("<-" denotes inheritance): - If Generic<Parent> <- Generic<Child>: it's covariant.
by branko_d 1y ago
Why? It's just a property of type transformation.
Assuming Parent <- Child ("<-" denotes inheritance):
- If Generic<Parent> <- Generic<Child>: it's covariant.
- If Generic<Parent> -> Generic<Child>: it's contravariant.
- Otherwise: it's invariant.
Or at least it's that straightforward in C#. Are there complications in Rust?
- tialaramex 1y agoSure, you can keep telling me that and it doesn't stay. I'm completely happy writing Rust, and I am aware it needs variance to work in principle and when I do need that information I know the magic words to type into doc search. It's like how I can hold in my head how classical DH KEX works and I can write a toy version with numbers that are too small - but for the actual KEX we use today, which is Elliptic Curve DH I'm like "Well, basically it's the same idea but the curves hurt my head so I just paste in somebody else's implementation" even in a toy. Sorry?
- dwattttt 1y agoOne day the rote example finally made sense to me, and I go back to it every time I hear about variance. Got a Container<Animal> and want to treat it as a Container<Cat>? Then you can only write to it: it's ok to put a Cat in a Container<Cat> that's really a Container<Animal>. Reading from it is wrong. If you treat a Container<Animal> like a Container<Cat> and read from it, you might get a Dog instead. The same works in reverse, treating a Container<Cat> like a Container<Animal> is ok for read, but not for write.
- zozbot234 1y agoThe usual way of phrasing this is, famously, https://en.wikipedia.org/wiki/A_white_horse_is_not_a_horse https://en.wikipedia.org/wiki/A_white_horse_is_not_a_horse
- Sharlin 1y agoIt's not particularly easy to teach what that actually means and why it's a thing. It's quite easy to show why in general G<A> cannot be a subtype of G<B> even if A is a subtype of B, it's rather more involved pedagogically to explain when it can, and even more confusing when it's actually the other way around (contravariance). Anyway, Rust has no subtyping except for lifetimes ('a <: 'b iff 'a lasts at least as long as 'b) , so variance only arises in very advanced use cases.
- smallstepforman 1y agoI’m getting old. I can understand the words, but not the content. At this point, show me dissassembly so that I can understand what actually happens on the fundamental byte/cpu instruction level, then I can figure out what you’re trying to explain.
- Sharlin 1y agoNothing really happens on the instruction level because this is all type system logic.
- Grayskull 1y agoI think this might be the issue. If something has zero effect in the end, why should I care about in the first place?
- bobbylarrybobby 1y agoVariance doesn't affect generated code because it acts earlier than that: determining whether code is valid or not and in doing so preventing invalid code (UB) from being compiled in the first place. The simplest example of incorrect variance → UB is that `&'a mut T` must be invariant in T. If it were covariant, you could take a `&'a mut &'static T`, write a `&'b T` into it for some non-static lifetime `'b` (since `'static: 'b` for all `'b`), and then... kaboom. 'b ends but the compiler thought this was a `&'a mut &'static T`, and you've got a dangling reference. `&'a mut T` can't be covariant in T for a similar reason: if you start with a `&'a mut &'b T`, contravariance would let you cast it to a `&'a mut &'static T`, and then you'd have a `&'static T` derived from a `&'b T`, which is again kaboom territory. So, variance’s effect is to guide the compiler and prevent dangling references from occurring at runtime by making code that produces them invalid. Neither of the above issues is observable at runtime (barring compiler bugs) precisely because the compiler enforces variance correctly.
- sesm 1y agoIt wad easy to teach to Java devs just by using one mnemonic: PECS = Producer Extends, Consumer Super
- kannanvijayan 1y agoThe difficulty is that even trivial generic types aren't cleanly one or the other. A mutable reference type is covariant on read, and contra on write. Scala was the first language in my exposure to try to simplify that away by lifting the variance annotations into the type parameter directly. It reduced some of the power but it made things easier to understand for developers. A full variance model would annotate specific features (methods) of a type as co/contra/invariant. I'm not sure what approach C# takes - I haven't looked into it. Rust doesn't expose variances for data structures at all. It exposes them for traits (type classes) and lifetimes, but neither of those are accessible in a higher order form. Trait usage is highly constrained and so is lifetime usage. Traits mainly can be used as bounds on types. Some subset of traits, characterized as object traits, can be used as types themselves, but only in an indirect context. These are highly restricted. For example, if you have traits T and U where U extends T, and you have a reference `&dyn U`, you can't convert that to a `&dyn T` without knowledge of the underlying concrete type. You _can_ convert `&A where A: U` to `&B where B: T`, but that just falls out of the fact that the compiler has access to all the concrete type and trait definitions there and can validate the transform. Rust's inheritance/variance story is a bit weird. They've kept it very minimal and constrained.
- zozbot234 1y agoNote that the Rust folks are working on a "safe-transmute" facility that may end up introducing a kind of variance to the language.
- James_K 1y ago> 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.