diff --git a/src/subtyping.md b/src/subtyping.md index b5ce948..0e33658 100644 --- a/src/subtyping.md +++ b/src/subtyping.md @@ -3,53 +3,135 @@ Subtyping is a relationship between types that allows statically typed languages to be a bit more flexible and permissive. -The most common and easy to understand example of this can be found in -languages with inheritance. Consider an Animal type which has an `eat()` -method, and a Cat type which extends Animal, adding a `meow()` method. -Without subtyping, if someone were to write a `feed(Animal)` function, they -wouldn't be able to pass a Cat to this function, because a Cat isn't *exactly* -an Animal. But being able to pass a Cat where an Animal is expected seems -fairly reasonable. After all, a Cat is just an Animal *and more*. Something -having extra features that can be ignored shouldn't be any impediment to -using it! - -This is exactly what subtyping lets us do. Because a Cat is an Animal *and more* -we say that Cat is a *subtype* of Animal. We then say that anywhere a value of -a certain type is expected, a value with a subtype can also be supplied. Ok -actually it's a lot more complicated and subtle than that, but that's the -basic intuition that gets you by in 99% of the cases. We'll cover why it's -*only* 99% later in this section. - -Although Rust doesn't have any notion of structural inheritance, it *does* -include subtyping. In Rust, subtyping derives entirely from lifetimes. Since -lifetimes are regions of code, we can partially order them based on the -*contains* (outlives) relationship. - -Subtyping on lifetimes is in terms of that relationship: if `'big: 'small` -("big contains small" or "big outlives small"), then `'big` is a subtype +Subtyping in Rust is a bit different from subtyping in other languages. This +makes it harder to give simple examples, which is a problem since subtyping, +and especially variance, are already hard to understand properly. As in, +even compiler writers mess it up all the time. + +To keep things simple, this section will consider a small extension to the +Rust language that adds a new and simpler subtyping relationship. After +establishing concepts and issues under this simpler system, +we will then relate it back to how subtyping actually occurs in Rust. + +So here's our simple extension, *Objective Rust*, featuring three new types: + + +```rust +trait Animal { + fn snuggle(&self); + fn eat(&mut self); +} + +trait Cat: Animal { + fn meow(&self); +} + +trait Dog: Animal { + fn bark(&self); +} +``` + +But unlike normal traits, we can use them as concrete and sized types, just like structs. + +Now, say we have a very simple function that takes an Animal, like this: + +```rust,ignore +fn love(pet: Animal) { + pet.snuggle(); +} +``` + +By default, static types must match *exactly* for a program to compile. As such, +this code won't compile: + +```rust,ignore +let mr_snuggles: Cat = ...; +love(mr_snuggles); // ERROR: expected Animal, found Cat +``` + +Mr. Snuggles is a Cat, and Cats aren't *exactly* Animals, so we can't love him! 😿 + +This is annoying because Cats *are* Animals. They support every operation +an Animal supports, so intuitively `love` shouldn't care if we pass it a `Cat`. +We should be able to just **forget** the non-animal parts of our `Cat`, as they +aren't necessary to love it. + +This is exactly the problem that *subtyping* is intended to fix. Because Cats are just +Animals **and more**, we say Cat is a *subtype* of Animal (because Cats are a *subset* +of all the Animals). Equivalently, we say that Animal is a *supertype* of Cat. +With subtypes, we can tweak our overly strict static type system +with a simple rule: anywhere a value of type `T` is expected, we will also +accept values that are subtypes of `T`. + +Or more concretely: anywhere an Animal is expected, a Cat or Dog will also work. + +As we will see throughout the rest of this section, subtyping is a lot more complicated +and subtle than this, but this simple rule is a very good 99% intuition. And unless you +write unsafe code, the compiler will automatically handle all the corner cases for you. + +But this is the Rustonomicon. We're writing unsafe code, so we need to understand how +this stuff really works, and how we can mess it up. + +The core problem is that this rule, naively applied, will lead to *meowing Dogs*. That is, +we can convince someone that a Dog is actually a Cat. This completely destroys the fabric +of our static type system, making it worse than useless (and leading to Undefined Behaviour). + +Here's a simple example of this happening when we apply subtyping in a completely naive +"find and replace" way. + +```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! +} +``` + +Clearly, we need a more robust system than "find and replace". That system is *variance*, +which is a set of rules governing how subtyping should compose. Most importantly, variance +defines situations where subtyping should be disabled. + +But before we get into variance, let's take a quick peek at where subtyping actually occurs in +Rust: *lifetimes*! + +> NOTE: The typed-ness of lifetimes is a fairly arbitrary construct that some +> disagree with. However it simplifies our analysis to treat lifetimes +> and types uniformly. + +Lifetimes are just regions of code, and regions can be partially ordered with the *contains* +(outlives) relationship. Subtyping on lifetimes is in terms of that relationship: +if `'big: 'small` ("big contains small" or "big outlives small"), then `'big` is a subtype of `'small`. This is a large source of confusion, because it seems backwards to many: the bigger region is a *subtype* of the smaller region. But it makes -sense if you consider our Animal example: *Cat* is an Animal *and more*, +sense if you consider our Animal example: Cat is an Animal *and more*, just as `'big` is `'small` *and more*. Put another way, if someone wants a reference that lives for `'small`, usually what they actually mean is that they want a reference that lives for *at least* `'small`. They don't actually care if the lifetimes match -exactly. For this reason `'static`, the forever lifetime, is a subtype -of every lifetime. +exactly. So it should be ok for us to **forget** that something lives for +`'big` and only remember that it lives for `'small`. -Higher-ranked lifetimes are also subtypes of every concrete lifetime. This is -because taking an arbitrary lifetime is strictly more general than taking a -specific one. +The meowing dog problem for lifetimes will result in us being able to +store a short-lived reference in a place that expects a longer-lived one, +creating a dangling reference and letting us use-after-free. -(The typed-ness of lifetimes is a fairly arbitrary construct that some -disagree with. However it simplifies our analysis to treat lifetimes -and types uniformly.) - -However you can't write a function that takes a value of type `'a`! Lifetimes -are always just part of another type, so we need a way of handling that. -To handle it, we need to talk about *variance*. +It will be useful to note that `'static`, the forever lifetime, is a subtype of +every lifetime because by definition it outlives everything. We will be using +this relationship in later examples to keep them as simple as possible. +With all that said, we still have no idea how to actually *use* subtyping of lifetimes, +because nothing ever has type `'a`. Lifetimes only occur as part of some larger type +like `&'a u32` or `IterMut<'a, u32>`. To apply lifetime subtyping, we need to know +how to compose subtyping. Once again, we need *variance*. @@ -59,200 +141,288 @@ To handle it, we need to talk about *variance*. Variance is where things get a bit complicated. Variance is a property that *type constructors* have with respect to their -arguments. A type constructor in Rust is a generic type with unbound arguments. -For instance `Vec` is a type constructor that takes a `T` and returns a +arguments. A type constructor in Rust is any generic type with unbound arguments. +For instance `Vec` is a type constructor that takes a type `T` and returns `Vec`. `&` and `&mut` are type constructors that take two inputs: a lifetime, and a type to point to. +> NOTE: For convenience we will often refer to `F` as a type constructor just so +> that we can easily talk about `T`. Hopefully this is clear in context. + A type constructor F's *variance* is how the subtyping of its inputs affects the -subtyping of its outputs. There are three kinds of variance in Rust: +subtyping of its outputs. There are three kinds of variance in Rust. Given two +types `Sub` and `Super`, where `Sub` is a subtype of `Super`: + +* `F` is *covariant* if `F` is a subtype of `F` (subtyping "passes through") +* `F` is *contravariant* if `F` is a subtype of `F` (subtyping is "inverted") +* `F` is *invariant* otherwise (no subtyping relationship exists) + +If `F` has multiple type parameters, we can talk about the individual variances +by saying that, for example, `F` is covariant over `T` and invariant over `U`. + +It is very useful to keep in mind that covariance is, in practical terms, "the" +variance. Almost all consideration of variance is in terms of whether something +should be covariant or invariant. Actually witnessing contravariance is quite difficult +in Rust, though it does in fact exist. + +Here is a table of important variances which the rest of this section will be devoted +to trying to explain: + +| | | 'a | T | U | +|---|-----------------|:---------:|:-----------------:|:---------:| +| * | `&'a T ` | covariant | covariant | | +| * | `&'a mut T` | covariant | invariant | | +| * | `Box` | | covariant | | +| | `Vec` | | covariant | | +| * | `UnsafeCell` | | invariant | | +| | `Cell` | | invariant | | +| * | `fn(T) -> U` | | **contra**variant | covariant | +| | `*const T` | | covariant | | +| | `*mut T` | | invariant | | + +The types with \*'s are the ones we will be focusing on, as they are in +some sense "fundamental". All the others can be understood by analogy to the others: + +* Vec and all other owning pointers and collections follow the same logic as Box +* Cell and all other interior mutability types follow the same logic as UnsafeCell +* `*const` follows the logic of `&T` +* `*mut` follows the logic of `&mut T` (or `UnsafeCell`) + +> NOTE: the *only* source of contravariance in the language is the arguments to +> a function, which is why it really doesn't come up much in practice. Invoking +> contravariance involves higher-order programming with function pointers that +> take references with specific lifetimes (as opposed to the usual "any lifetime", +> which gets into higher rank lifetimes, which work independently of subtyping). + +Ok, that's enough type theory! Let's try to apply the concept of variance to Rust +and look at some examples. + +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! +} +``` -* F is *covariant* over `T` if `T` being a subtype of `U` implies - `F` is a subtype of `F` (subtyping "passes through") -* F is *contravariant* over `T` if `T` being a subtype of `U` implies - `F` is a subtype of `F` (subtyping is "inverted") -* F is *invariant* over `T` otherwise (no subtyping relation can be derived) +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`. -It should be noted that covariance is *far* more common and important than -contravariance in Rust. The existence of contravariance in Rust can mostly -be ignored. +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. -Some important variances (which we will explain in detail below): +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. -* `&'a T` is covariant over `'a` and `T` (as is `*const T` by metaphor) -* `&'a mut T` is covariant over `'a` but invariant over `T` -* `fn(T) -> U` is **contra**variant over `T`, but covariant over `U` -* `Box`, `Vec`, and all other collections are covariant over the types of - their contents -* `UnsafeCell`, `Cell`, `RefCell`, `Mutex` and all other - interior mutability types are invariant over T (as is `*mut T` by metaphor) +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`! -To understand why these variances are correct and desirable, we will consider -several examples. +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: -We have already covered why `&'a T` should be covariant over `'a` when -introducing subtyping: it's desirable to be able to pass longer-lived things -where shorter-lived things are needed. +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! -Similar reasoning applies to why it should be covariant over T: it's reasonable -to be able to pass `&&'static str` where an `&&'a str` is expected. The -additional level of indirection doesn't change the desire to be able to pass -longer lived things where shorter lived things are expected. +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 +anyway. There are now two independent references with independent lifetimes. +There's no way to mess with original reference's lifetime using the other one. -However this logic doesn't apply to `&mut`. To see why `&mut` should -be invariant over T, consider the following code: +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: ```rust,ignore -fn overwrite(input: &mut T, new: &mut T) { - *input = *new; +fn evil_feeder(input: &mut T, val: T) { + *input = val; } fn main() { - let mut forever_str: &'static str = "hello"; + let mut mr_snuggles: &'static str = "meow! :3"; // mr. snuggles forever!! { - let string = String::from("world"); - overwrite(&mut forever_str, &mut &*string); + let spike = String::from("bark! >:V"); + let spike_str: &str = &spike; // Only lives for the block + evil_feeder(&mut mr_snuggles, spike_str); // EVIL! } - // Oops, printing free'd memory - println!("{}", forever_str); + println!("{}", mr_snuggles); // Use after free? } ``` -The signature of `overwrite` is clearly valid: it takes mutable references to -two values of the same type, and overwrites one with the other. - -But, if `&mut T` was covariant over T, then `&mut &'static str` would be a -subtype of `&mut &'a str`, since `&'static str` is a subtype of `&'a str`. -Therefore the lifetime of `forever_str` would successfully be "shrunk" down -to the shorter lifetime of `string`, and `overwrite` would be called successfully. -`string` would subsequently be dropped, and `forever_str` would point to -freed memory when we print it! Therefore `&mut` should be invariant. - -This is the general theme of variance vs invariance: if variance would allow you -to store a short-lived value in a longer-lived slot, then invariance must be used. - -More generally, the soundness of subtyping and variance is based on the idea that its ok to -forget details, but with mutable references there's always someone (the original -value being referenced) that remembers the forgotten details and will assume -that those details haven't changed. If we do something to invalidate those details, -the original location can behave unsoundly. - -However it *is* sound for `&'a mut T` to be covariant over `'a`. The key difference -between `'a` and T is that `'a` is a property of the reference itself, -while T is something the reference is borrowing. If you change T's type, then -the source still remembers the original type. However if you change the -lifetime's type, no one but the reference knows this information, so it's fine. -Put another way: `&'a mut T` owns `'a`, but only *borrows* T. - -`Box` and `Vec` are interesting cases because they're covariant, but you can -definitely store values in them! This is where Rust's typesystem allows it to -be a bit more clever than others. To understand why it's sound for owning -containers to be covariant over their contents, we must consider -the two ways in which a mutation may occur: by-value or by-reference. - -If mutation is by-value, then the old location that remembers extra details is -moved out of, meaning it can't use the value anymore. So we simply don't need to -worry about anyone remembering dangerous details. Put another way, applying -subtyping when passing by-value *destroys details forever*. For example, this -compiles and is fine: +And what do we get when we run this? + +```text +error[E0597]: `spike` does not live long enough + --> src/main.rs:9:32 + | +9 | let spike_str: &str = &spike; + | ^^^^^ borrowed value does not live long enough +10 | evil_feeder(&mut mr_snuggles, spike_str); +11 | } + | - borrowed value only lives until here + | + = note: borrowed value must be valid for the static lifetime... +``` + +Good, it doesn't compile! Let's break down what's happening here in detail. + +First let's look at the new `evil_feeder` function: ```rust -fn get_box<'a>(str: &'a str) -> Box<&'a str> { - // String literals are `&'static str`s, but it's fine for us to - // "forget" this and let the caller think the string won't live that long. - Box::new("hello") +fn evil_feeder(input: &mut T, val: T) { + *input = val; } ``` -If mutation is by-reference, then our container is passed as `&mut Vec`. But -`&mut` is invariant over its value, so `&mut Vec` is actually invariant over `T`. -So the fact that `Vec` is covariant over `T` doesn't matter at all when -mutating by-reference. +All it does it take a mutable reference and a value and overwrite the referent with it. +What's important about this function is that it creates a type equality constraint. It +clearly says in its signature the referent and the value must be the *exact same* type. -But being covariant still allows `Box` and `Vec` to be weakened when shared -immutably. So you can pass a `&Vec<&'static str>` where a `&Vec<&'a str>` is -expected. +Meanwhile, in the caller we pass in `&mut &'static str` and `&'spike_str str`. -The invariance of the cell types can be seen as follows: `&` is like an `&mut` -for a cell, because you can still store values in them through an `&`. Therefore -cells must be invariant to avoid lifetime smuggling. +Because `&mut T` is invariant over `T`, the compiler concludes it can't apply any subtyping +to the first argument, and so `T` must be exactly `&'static str`. -`fn` is the most subtle case because they have mixed variance, and in fact are -the only source of **contra**variance. To see why `fn(T) -> U` should be contravariant -over T, consider the following function signature: +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! -```rust,ignore -// 'a is derived from some parent scope -fn foo(&'a str) -> usize; -``` +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`. -This signature claims that it can handle any `&str` that lives at least as -long as `'a`. Now if this signature was **co**variant over `&'a str`, that -would mean +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 +lifetimes 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 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 -fn foo(&'static str) -> usize; +let mr_snuggles: Box = ..; +let spike: Box = ..; + +let mut pet: Box; +pet = mr_snuggles; +pet = spike; ``` -could be provided in its place, as it would be a subtype. However this function -has a stronger requirement: it says that it can only handle `&'static str`s, -and nothing else. Giving `&'a str`s to it would be unsound, as it's free to -assume that what it's given lives forever. Therefore functions definitely shouldn't -be **co**variant over their arguments. +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**! -However if we flip it around and use **contra**variance, it *does* work! If -something expects a function which can handle strings that live forever, -it makes perfect sense to instead provide a function that can handle -strings that live for *less* than forever. So +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! + +Only one thing left to explain: function pointers. + +To see why `fn(T) -> U` should be covariant over `U`, consider the following signature: ```rust,ignore -fn foo(&'a str) -> usize; +fn get_animal() -> Animal; ``` -can be passed where +This function claims to produce an Animal. As such, it is perfectly valid to +provide a function with the following signature instead: ```rust,ignore -fn foo(&'static str) -> usize; +fn get_animal() -> Cat; ``` -is expected. +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. -To see why `fn(T) -> U` should be **co**variant over U, consider the following -function signature: +However, the same logic does not apply to *arguments*. Consider trying to satisfy: ```rust,ignore -// 'a is derived from some parent scope -fn foo(usize) -> &'a str; +fn handle_animal(Animal); ``` -This signature claims that it will return something that outlives `'a`. It is -therefore completely reasonable to provide +with ```rust,ignore -fn foo(usize) -> &'static str; +fn handle_animal(Cat); ``` -in its place, as it does indeed return things that outlive `'a`. Therefore -functions are covariant over their return type. +The first function can accept Dogs, but the second function absolutely can't. +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`. -`*const` has the exact same semantics as `&`, so variance follows. `*mut` on the -other hand can dereference to an `&mut` whether shared or not, so it is marked -as invariant just like cells. +And that's why function types, unlike anything else in the language, are +**contra**variant over their arguments. -This is all well and good for the types the standard library provides, but +Now, this is all well and good for the types the standard library provides, but how is variance determined for type that *you* define? A struct, informally -speaking, inherits the variance of its fields. If a struct `Foo` -has a generic argument `A` that is used in a field `a`, then Foo's variance -over `A` is exactly `a`'s variance. However if `A` is used in multiple fields: +speaking, inherits the variance of its fields. If a struct `MyType` +has a generic argument `A` that is used in a field `a`, then MyType's variance +over `A` is exactly `a`'s variance over `A`. -* If all uses of A are covariant, then Foo is covariant over A -* If all uses of A are contravariant, then Foo is contravariant over A -* Otherwise, Foo is invariant over A +However if `A` is used in multiple fields: + +* If all uses of `A` are covariant, then MyType is covariant over `A` +* If all uses of `A` are contravariant, then MyType is contravariant over `A` +* Otherwise, MyType is invariant over `A` ```rust use std::cell::Cell; -struct Foo<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed> { +struct MyType<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed> { a: &'a A, // covariant over 'a and A b: &'b mut B, // covariant over 'b and invariant over B @@ -272,3 +442,4 @@ struct Foo<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed> { k2: Mixed, // invariant over Mixed, because invariance wins all conflicts } ``` +