From b28d364f0fd111122162e9c66876b139acc207f0 Mon Sep 17 00:00:00 2001 From: Alexis Beingessner Date: Fri, 9 Nov 2018 01:36:36 -0500 Subject: [PATCH 1/3] Subtyping rewrite --- src/subtyping.md | 503 +++++++++++++++++++++++++++++++---------------- 1 file changed, 334 insertions(+), 169 deletions(-) diff --git a/src/subtyping.md b/src/subtyping.md index b5ce948..41cf17a 100644 --- a/src/subtyping.md +++ b/src/subtyping.md @@ -3,53 +3,133 @@ 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 leads +to examples of subtyping being *a bit* convoluted. This is especially unforunate +because subtyping, and especially variance, are actually really hard to +understand properly. As in, even compiler writers mess it up all the time. + +In order to make examples that are simple and concise, this section will consider a +*small* extension to the Rust language to introduce subtyping in a way that is more +similar to other languages. 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 especially annoying because Cats *are* Animals. They support every operation +an Animal supports, so intuitively `love` shouldn't care if we pass it a `Cat`. +Or, to put it another way, 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 can 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 some type `T` is expected, we *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*, +the rules governing how subtyping *composes*. + +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. - -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. +exactly. So it should be ok for us to *forget* that we live for `'big`. -(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.) +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. -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 frequently in examples. +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 +139,284 @@ 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 -`Vec`. `&` and `&mut` are type constructors that take two inputs: 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 `X` 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 | | + +These 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; +} -* 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) +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! +} +``` -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. +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`. -Some important variances (which we will explain in detail below): +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. -* `&'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) +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. -To understand why these variances are correct and desirable, we will consider -several examples. +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 anyway 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`! -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. +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: -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. +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! -However this logic doesn't apply to `&mut`. To see why `&mut` should -be invariant over T, consider the following code: +Second, and more seriously, references *own* their lifetimes, while they are +only borrowing their referents. If you shrink down a reference's lifetime when +you hand it to someone, that location now has a reference which owns the smaller +lifetime. There's no way to mess with 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, +and as soon as you try to build a meowing dog the lifetime should be wrapped up +in an invariant context, preventing the lifetime from being shrunk. That's probably +a little too abstract, so 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: +`&'a str` *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 subtype `'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 the 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 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 the *owning* aspect of references that we +partially glossed over. -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 +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 +rememembered 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 +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. However if `A` is used in multiple fields: -* 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 +* 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 +436,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 } ``` + From 6ec81e17f6b983f59b261acb99d60efa32a031fa Mon Sep 17 00:00:00 2001 From: Alexis Beingessner Date: Fri, 9 Nov 2018 12:52:49 -0500 Subject: [PATCH 2/3] cleanup --- src/subtyping.md | 110 +++++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/src/subtyping.md b/src/subtyping.md index 41cf17a..f0caf71 100644 --- a/src/subtyping.md +++ b/src/subtyping.md @@ -3,15 +3,15 @@ Subtyping is a relationship between types that allows statically typed languages to be a bit more flexible and permissive. -Subtyping in Rust is a bit different from subtyping in other languages. This leads -to examples of subtyping being *a bit* convoluted. This is especially unforunate -because subtyping, and especially variance, are actually really hard to -understand properly. As in, even compiler writers mess it up all the time. +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. -In order to make examples that are simple and concise, this section will consider a -*small* extension to the Rust language to introduce subtyping in a way that is more -similar to other languages. After establishing concepts and issues under this simpler -system, we will then relate it back to how subtyping actually occurs in Rust. +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: @@ -51,17 +51,17 @@ 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 especially annoying because Cats *are* Animals. They support every operation +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`. -Or, to put it another way, we should be able to just *forget* the non-animal -parts of our `Cat`, as they aren't necessary to love it. +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 can say that Animal is a *supertype* of Cat. +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 some type `T` is expected, we *also* accept -values that are *subtypes* of `T`. +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. @@ -72,7 +72,7 @@ write unsafe code, the compiler will automatically handle all the corner cases f 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, +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). @@ -96,7 +96,8 @@ fn main() { ``` Clearly, we need a more robust system than "find and replace". That system is *variance*, -the rules governing how subtyping *composes*. +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*! @@ -116,7 +117,8 @@ 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. So it should be ok for us to *forget* that we live for `'big`. +exactly. So it should be ok for us to **forget** that something lives for +`'big` and only remember that it lives for `'small`. 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, @@ -124,12 +126,12 @@ creating a dangling reference and letting us use-after-free. 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 frequently in examples. +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*. +how to compose subtyping. Once again, we need *variance*. @@ -140,8 +142,8 @@ 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 any generic type with unbound arguments. -For instance `Vec` is a type constructor that takes a type `X` and returns -`Vec`. `&` and `&mut` are type constructors that take two inputs: a +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 @@ -158,7 +160,7 @@ types `Sub` and `Super`, where `Sub` is a subtype of `Super`: 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" +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. @@ -178,7 +180,7 @@ to trying to explain: | | *const T | | covariant | | | | *mut T | | invariant | | -These types with \*'s are the ones we will be focusing on, as they are in +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 @@ -190,9 +192,9 @@ some sense "fundamental". All the others can be understood by analogy to the oth > 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). +> 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, +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: @@ -229,28 +231,29 @@ 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 anyway to +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 +`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: +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, references *own* their lifetimes, while they are -only borrowing their referents. If you shrink down a reference's lifetime when -you hand it to someone, that location now has a reference which owns the smaller -lifetime. There's no way to mess with original reference's lifetime using the -other one. +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. -Or rather, the only way to mess with someone's lifetime is to build a meowing dog, -and as soon as you try to build a meowing dog the lifetime should be wrapped up -in an invariant context, preventing the lifetime from being shrunk. That's probably -a little too abstract, so let's port the meowing dog problem over to real Rust. +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 @@ -293,7 +296,7 @@ error[E0597]: `spike` does not live long enough = 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. +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: @@ -312,12 +315,12 @@ Meanwhile, in the caller we pass in `&mut &'static str` and `&'spike_str str`. 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`. -`&'a str` *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 subtype `'static` (inclusive). Which is to say, `'spike_str` must contain -`'static`. But only one thing contains `'static` -- `'static` itself! +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 the error when we try to assign `&spike` to `spike_str`. The +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. @@ -326,12 +329,12 @@ whenever they're put into a context that could do something bad with that. In th 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 why it's ok for +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! +bad. -However Box makes it easier to focus on the *owning* aspect of references that we +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, @@ -351,8 +354,8 @@ 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 -rememembered he was a Cat*! +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 @@ -361,8 +364,9 @@ 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: +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 get_animal() -> Animal; @@ -407,7 +411,9 @@ 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 `MyType` has a generic argument `A` that is used in a field `a`, then MyType's variance -over `A` is exactly `a`'s variance. However if `A` is used in multiple fields: +over `A` is exactly `a`'s variance 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` From a805a667ba8534b78b9587ba7644dac53ce0ab98 Mon Sep 17 00:00:00 2001 From: Alexis Beingessner Date: Fri, 9 Nov 2018 14:15:50 -0500 Subject: [PATCH 3/3] fixup --- src/subtyping.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/subtyping.md b/src/subtyping.md index f0caf71..0e33658 100644 --- a/src/subtyping.md +++ b/src/subtyping.md @@ -168,17 +168,17 @@ 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 | | +| | | '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: