diff --git a/src/subtyping.md b/src/subtyping.md index 0bda409..61bebcb 100644 --- a/src/subtyping.md +++ b/src/subtyping.md @@ -241,127 +241,9 @@ So the compiler decides that `&'static str` can become `&'b str` if and only if `&'static str` is a subtype of `&'b str`, which will hold if `'static: 'b`. This is true, so the compiler is happy to continue compiling this code. ---- - -First off, let's revisit the meowing dog example: - - -```rust,ignore -fn evil_feeder(pet: &mut Animal) { - let spike: Dog = ...; - - // `pet` is an Animal, and Dog is a subtype of Animal, - // so this should be fine, right..? - *pet = spike; -} - -fn main() { - let mut mr_snuggles: Cat = ...; - evil_feeder(&mut mr_snuggles); // Replaces mr_snuggles with a Dog - mr_snuggles.meow(); // OH NO, MEOWING DOG! -} -``` - -If we look at our table of variances, we see that `&mut T` is *invariant* over `T`. -As it turns out, this completely fixes the issue! With invariance, the fact that -Cat is a subtype of Animal doesn't matter; `&mut Cat` still won't be a subtype of -`&mut Animal`. The static type checker will then correctly stop us from passing -a Cat into `evil_feeder`. - -The soundness of subtyping is based on the idea that it's ok to forget unnecessary -details. But with references, there's always someone that remembers those details: -the value being referenced. That value expects those details to keep being true, -and may behave incorrectly if its expectations are violated. - -The problem with making `&mut T` covariant over `T` is that it gives us the power -to modify the original value *when we don't remember all of its constraints*. -And so, we can make someone have a Dog when they're certain they still have a Cat. - -With that established, we can easily see why `&T` being covariant over `T` *is* -sound: it doesn't let you modify the value, only look at it. Without any way to -mutate, there's no way for us to mess with any details. We can also see why -`UnsafeCell` and all the other interior mutability types must be invariant: they -make `&T` work like `&mut T`! - -Now what about the lifetime on references? Why is it ok for both kinds of references -to be covariant over their lifetimes? Well, here's a two-pronged argument: - -First and foremost, subtyping references based on their lifetimes is *the entire point -of subtyping in Rust*. The only reason we have subtyping is so we can pass -long-lived things where short-lived things are expected. So it better work! - -Second, and more seriously, lifetimes are only a part of the reference itself. The -type of the referent is shared knowledge, which is why adjusting that type in only -one place (the reference) can lead to issues. But if you shrink down a reference's -lifetime when you hand it to someone, that lifetime information isn't shared in -any way. There are now two independent references with independent lifetimes. -There's no way to mess with the original reference's lifetime using the other one. - -Or rather, the only way to mess with someone's lifetime is to build a meowing dog. -But as soon as you try to build a meowing dog, the lifetime should be wrapped up -in an invariant type, preventing the lifetime from being shrunk. To understand this -better, let's port the meowing dog problem over to real Rust. - -In the meowing dog problem we take a subtype (Cat), convert it into a supertype -(Animal), and then use that fact to overwrite the subtype with a value that satisfies -the constraints of the supertype but not the subtype (Dog). - -So with lifetimes, we want to take a long-lived thing, convert it into a -short-lived thing, and then use that to write something that doesn't live long -enough into the place expecting something long-lived. - -Here it is: - - -The other argument is only an `&'a str`, which *is* covariant over `'a`. So the compiler -adopts a constraint: `&'spike_str str` must be a subtype of `&'static str` (inclusive), -which in turn implies `'spike_str` must be a subtype of `'static` (inclusive). Which is to say, -`'spike_str` must contain `'static`. But only one thing contains `'static` -- `'static` itself! - -This is why we get an error when we try to assign `&spike` to `spike_str`. The -compiler has worked backwards to conclude `spike_str` must live forever, and `&spike` -simply can't live that long. - -So even though references are covariant over their lifetimes, they "inherit" invariance -whenever they're put into a context that could do something bad with that. In this case, -we inherited invariance as soon as we put our reference inside an `&mut T`. - -As it turns out, the argument for why it's ok for Box (and Vec, Hashmap, etc.) to -be covariant is pretty similar to the argument for why it's ok for -references to be covariant: as soon as you try to stuff them in something like a -mutable reference, they inherit invariance and you're prevented from doing anything -bad. - -However, Box makes it easier to focus on the by-value aspect of references that we -partially glossed over. - -Unlike a lot of languages which allow values to be freely aliased at all times, -Rust has a very strict rule: if you're allowed to mutate or move a value, you -are guaranteed to be the only one with access to it. - -Consider the following code: - - -```rust,ignore -let mr_snuggles: Box = ..; -let spike: Box = ..; - -let mut pet: Box; -pet = mr_snuggles; -pet = spike; -``` - -There is no problem at all with the fact that we have forgotten that `mr_snuggles` was a Cat, -or that we overwrote him with a Dog, because as soon as we moved mr_snuggles to a variable -that only knew he was an Animal, **we destroyed the only thing in the universe that -remembered he was a Cat**! - -In contrast to the argument about immutable references being soundly covariant because they -don't let you change anything, owned values can be covariant because they make you -change *everything*. There is no connection between old locations and new locations. -Applying by-value subtyping is an irreversible act of knowledge destruction, and -without any memory of how things used to be, no one can be tricked into acting on -that old information! +`Box` is also *covariant* over `T`. This would make sense, since it's supposed to be +usable the same as `&T`. If you try to mutate the box, you'll need a `&mut Box` and the +invariance of `&mut` will kick in here. Only one thing left to explain: function pointers. @@ -369,43 +251,40 @@ To see why `fn(T) -> U` should be covariant over `U`, consider the following sig ```rust,ignore -fn get_animal() -> Animal; +fn get_str() -> &'a str; ``` -This function claims to produce an Animal. As such, it is perfectly valid to +This function claims to produce a `str` bound by some liftime `'a`. As such, it is perfectly valid to provide a function with the following signature instead: ```rust,ignore -fn get_animal() -> Cat; +fn get_static() -> &'static str; ``` -After all, Cats are Animals, so always producing a Cat is a perfectly valid way -to produce Animals. Or to relate it back to real Rust: if we need a function -that is supposed to produce something that lives for `'short`, it's perfectly -fine for it to produce something that lives for `'long`. We don't care, we can -just forget that fact. +So when the function is called, all it's expecting is a `&str` which lives at least the lifetime of `'a`, +it doesn't matter if the value actually lives longer. However, the same logic does not apply to *arguments*. Consider trying to satisfy: ```rust,ignore -fn handle_animal(Animal); +fn store_ref(&'a str); ``` with: ```rust,ignore -fn handle_animal(Cat); +fn store_static(&'static str); ``` -The first function can accept Dogs, but the second function absolutely can't. +The first function can accept any string reference as long as it lives at least for `'a`, +but the second cannot accept a string reference that lives for any duration less than `'static`, +which would cause a conflict. Covariance doesn't work here. But if we flip it around, it actually *does* -work! If we need a function that can handle Cats, a function that can handle *any* -Animal will surely work fine. Or to relate it back to real Rust: if we need a -function that can handle anything that lives for at least `'long`, it's perfectly -fine for it to be able to handle anything that lives for at least `'short`. +work! If we need a function that can handle `&'static str`, a function that can handle *any* reference lifetime +will surely work fine. And that's why function types, unlike anything else in the language, are **contra**variant over their arguments.