diff --git a/src/subtyping.md b/src/subtyping.md index 17a5656..9cbf2af 100644 --- a/src/subtyping.md +++ b/src/subtyping.md @@ -1,30 +1,57 @@ # Subtyping and Variance +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 scopes, we can partially order them based on the *contains* -(outlives) relationship. We can even express this as a generic bound. - -Subtyping on lifetimes is in terms of that relationship: if `'a: 'b` ("a contains -b" or "a outlives b"), then `'a` is a subtype of `'b`. This is a large source of -confusion, because it seems intuitively backwards to many: the bigger scope is a -*subtype* of the smaller scope. - -This does in fact make sense, though. The intuitive reason for this is that if -you expect an `&'a u8` (for some concrete `'a` that you have already chosen), -then it's totally fine for me to hand you an `&'static u8` even if `'static != -'a`, in the same way that if you expect an Animal in Java, it's totally fine -for me to hand you a Cat. Cats are just Animals *and more*, just as `'static` -is just `'a` *and more*. - -(Note, the subtyping relationship and 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 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 +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*, +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. +(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*. + + + # Variance @@ -38,25 +65,24 @@ For instance `Vec` is a type constructor that takes a `T` and returns a lifetime, and a type to point to. A type constructor's *variance* is how the subtyping of its inputs affects the -subtyping of its outputs. There are two kinds of variance in Rust: +subtyping of its outputs. There are three kinds of variance in Rust: -* F is *variant* over `T` if `T` being a subtype of `U` implies +* 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) -(For those of you who are familiar with variance from other languages, what we -refer to as "just" variance is in fact *covariance*. Rust has *contravariance* -for functions. The future of contravariance is uncertain and it may be -scrapped. For now, `fn(T)` is contravariant in `T`, which is used in matching -methods in trait implementations to the trait definition. Traits don't have -inferred variance, so `Fn(T)` is invariant in `T`). +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. -Some important variances: +Some important variances (which we will explain in detail below): -* `&'a T` is variant over `'a` and `T` (as is `*const T` by metaphor) -* `&'a mut T` is variant over `'a` but invariant over `T` -* `Fn(T) -> U` is invariant over `T`, but variant over `U` -* `Box`, `Vec`, and all other collections are variant over the types of +* `&'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) @@ -64,15 +90,14 @@ Some important variances: To understand why these variances are correct and desirable, we will consider several examples. - -We have already covered why `&'a T` should be variant over `'a` when +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. -Similar reasoning applies to why it should be variant over T. It is reasonable +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 does not change the desire to be able to pass -longer lived things where shorted lived things are expected. +additional level of indirection doesn't change the desire to be able to pass +longer lived things where shorter lived things are expected. However this logic doesn't apply to `&mut`. To see why `&mut` should be invariant over T, consider the following code: @@ -94,58 +119,67 @@ fn main() { ``` The signature of `overwrite` is clearly valid: it takes mutable references to -two values of the same type, and overwrites one with the other. If `&mut T` was -variant 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. +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 into a longer-lived slot, then you must be -invariant. +to store a short-lived value in a longer-lived slot, then invariance must be used. -However it *is* sound for `&'a mut T` to be variant over `'a`. The key difference +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 variant, but you can -definitely store values in them! This is where Rust gets really clever: it's -fine for them to be variant because you can only store values -in them *via a mutable reference*! The mutable reference makes the whole type -invariant, and therefore prevents you from smuggling a short-lived type into -them. - -Being variant allows `Box` and `Vec` to be weakened when shared -immutably. So you can pass a `&Box<&'static str>` where a `&Box<&'a str>` is -expected. +`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. -However what should happen when passing *by-value* is less obvious. It turns out -that, yes, you can use subtyping when passing by-value. That is, this works: +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: ```rust fn get_box<'a>(str: &'a str) -> Box<&'a str> { - // string literals are `&'static str`s + // 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") } ``` -Weakening when you pass by-value is fine because there's no one else who -"remembers" the old lifetime in the Box. The reason a variant `&mut` was -trouble was because there's always someone else who remembers the original -subtype: the actual owner. +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. + +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. 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. -`Fn` is the most subtle case because it has mixed variance. To see why -`Fn(T) -> U` should be invariant over T, consider the following function -signature: +`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: ```rust,ignore // 'a is derived from some parent scope @@ -153,7 +187,7 @@ fn foo(&'a str) -> usize; ``` This signature claims that it can handle any `&str` that lives at least as -long as `'a`. Now if this signature was variant over `&'a str`, that +long as `'a`. Now if this signature was **co**variant over `&'a str`, that would mean ```rust,ignore @@ -163,10 +197,27 @@ fn foo(&'static str) -> usize; 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 are not variant -over their arguments. +assume that what it's given lives forever. Therefore functions definitely shouldn't +be **co**variant over their arguments. + +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 -To see why `Fn(T) -> U` should be variant over U, consider the following +```rust,ignore +fn foo(&'a str) -> usize; +``` + +can be passed where + +```rust,ignore +fn foo(&'static str) -> usize; +``` + +is expected. + +To see why `fn(T) -> U` should be **co**variant over U, consider the following function signature: ```rust,ignore @@ -181,7 +232,8 @@ therefore completely reasonable to provide fn foo(usize) -> &'static str; ``` -in its place. Therefore functions are variant over their return type. +in its place, as it does indeed return things that outlive `'a`. Therefore +functions are covariant over their return type. `*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 @@ -191,24 +243,32 @@ 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 this is complicated if `A` is used -in multiple fields. +over `A` is exactly `a`'s variance. However if `A` is used in multiple fields: -* If all uses of A are variant, then Foo is variant 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 ```rust use std::cell::Cell; -struct Foo<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H> { - a: &'a A, // variant over 'a and A - b: &'b mut B, // variant over 'b and invariant over B - c: *const C, // variant over C +struct Foo<'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 + + c: *const C, // covariant over C d: *mut D, // invariant over D - e: Vec, // variant over E - f: Cell, // invariant over F - g: G, // variant over G + + e: E, // covariant over E + f: Vec, // covariant over F + g: Cell, // invariant over G + h1: H, // would also be variant over H except... - h2: Cell, // invariant over H, because invariance wins + h2: Cell, // invariant over H, because invariance wins all conflicts + + i: fn(In) -> Out, // contravariant over In, covariant over Out + + k1: fn(Mixed) -> usize, // would be contravariant over Mixed except.. + k2: Mixed, // invariant over Mixed, because invariance wins all conflicts } ```