diff --git a/src/subtyping.md b/src/subtyping.md index 3792cbe..0bda409 100644 --- a/src/subtyping.md +++ b/src/subtyping.md @@ -42,11 +42,11 @@ fn debug(a: T, b: T) { } fn main() { - let a: &'static str = "hello"; + let hello: &'static str = "hello"; { - let b = String::from("world"); - let b = &b; // 'b has a shorter lifetime than 'static - debug(a, b); + let world = String::from("world"); + let world = &world; // 'b has a shorter lifetime than 'static + debug(hello, world); } } ``` @@ -58,10 +58,10 @@ we might see the following error: error[E0308]: mismatched types --> src/main.rs:10:16 | -10 | debug(a, b); - | ^ - | | - | expected `&'static str`, found struct `&'b str` +10 | debug(hello, world); + | ^ + | | + | expected `&'static str`, found struct `&'b str` ``` This is over-restrictive. In this case, what we want is to accept any type that lives *at least as long* as `'b`. @@ -83,11 +83,11 @@ fn debug(a: T, b: T) { } fn main() { - let a: &'static str = "hello"; + let hello: &'static str = "hello"; { - let b = String::from("world"); - let b = &b; // 'b has a shorter lifetime than 'static - debug(a, b); // a silently converts from `&'static str` into `&'b str` + let world = String::from("world"); + let world = &world; // 'b has a shorter lifetime than 'static + debug(hello, world); // a silently converts from `&'static str` into `&'b str` } } ``` @@ -98,26 +98,25 @@ Above, we glossed over the fact that `'static: 'b` implied that `&'static T: &'b It's not always as simple as this example though, to understand that let's try extend this example a bit ```rust,compile_fail -fn debug(a: &mut T, b: T) { - *a = b; +fn assign(input: &mut T, val: T) { + *input = val; } fn main() { - let mut a: &'static str = "hello"; + let mut hello: &'static str = "hello"; { - let b = String::from("world"); - let b = &b; - debug(&mut a, b); + let world = String::from("world"); + assign(&mut hello, &world); } } ``` -This has a memory bug in it. +If this were to compile, this would have a memory bug. If we were to expand this out, we'd see that we're trying to assign a `&'b str` into a `&'static str`, but the problem is that as soon as `b` goes out of scope, `a` is now invalid, even though it's supposed to have a `'static` lifetime. -However, the implementation of `debug` is valid. +However, the implementation of `assign` is valid. Therefore, this must mean that `&mut &'static str` should **not** a *subtype* of `&mut &'b str`, even if `'static` is a subtype of `'b`. @@ -149,21 +148,21 @@ Here is a table of some other type constructors and their variances: | | | 'a | T | U | |---|-----------------|:---------:|:-----------------:|:---------:| -| * | `&'a T ` | covariant | covariant | | -| * | `&'a mut T` | covariant | invariant | | -| * | `Box` | | covariant | | +| | `&'a T ` | covariant | covariant | | +| | `&'a mut T` | covariant | invariant | | +| | `Box` | | covariant | | | | `Vec` | | covariant | | -| * | `UnsafeCell` | | invariant | | +| | `UnsafeCell` | | invariant | | | | `Cell` | | invariant | | -| * | `fn(T) -> U` | | **contra**variant | covariant | +| | `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: +Some of these can be explained simply in relation 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` +* `UnsafeCell` having interior mutability gives it the same variance properties as `&mut T` * `*const T` follows the logic of `&T` * `*mut T` follows the logic of `&mut T` (or `UnsafeCell`) @@ -177,8 +176,72 @@ For more types, see the ["Variance" section][variance-table] on the reference. > 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. +Now that we have some more formal understanding of variance, +let's go through some more examples in more detail. + +```rust,compile_fail +fn assign(input: &mut T, val: T) { + *input = val; +} + +fn main() { + let mut hello: &'static str = "hello"; + { + let world = String::from("world"); + assign(&mut hello, &world); + } +} +``` + +And what do we get when we run this? + +```text +error[E0597]: `world` does not live long enough + --> src/main.rs:9:28 + | +6 | let mut hello: &'static str = "hello"; + | ------------ type annotation requires that `world` is borrowed for `'static` +... +9 | assign(&mut hello, &world); + | ^^^^^^ borrowed value does not live long enough +10 | } + | - `world` dropped here while still borrowed +``` + +Good, it doesn't compile! Let's break down what's happening here in detail. + +First let's look at the `assign` function: + +```rust +fn assign(input: &mut T, val: T) { + *input = val; +} +``` + +All it does is 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. + +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`. + +This is counter to the `&T` case + +```rust +fn debug(a: T, b: T) { + println!("a = {:?} b = {:?}", a, b); +} +``` + +Where similarly `a` and `b` must have the same type `T`. +But since `&'a T` *is* covariant over `'a`, we are allowed to perform subtyping. +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: @@ -249,56 +312,6 @@ enough into the place expecting something long-lived. Here it is: -```rust,compile_fail -fn evil_feeder(input: &mut T, val: T) { - *input = val; -} - -fn main() { - let mut mr_snuggles: &'static str = "meow! :3"; // mr. snuggles forever!! - { - let spike = String::from("bark! >:V"); - let spike_str: &str = &spike; // Only lives for the block - evil_feeder(&mut mr_snuggles, spike_str); // EVIL! - } - println!("{}", mr_snuggles); // Use after free? -} -``` - -And what do we get when we run this? - -```text -error[E0597]: `spike` does not live long enough - --> src/main.rs:9:31 - | -6 | let mut mr_snuggles: &'static str = "meow! :3"; // mr. snuggles forever!! - | ------------ type annotation requires that `spike` is borrowed for `'static` -... -9 | let spike_str: &str = &spike; // Only lives for the block - | ^^^^^^ borrowed value does not live long enough -10 | evil_feeder(&mut mr_snuggles, spike_str); // EVIL! -11 | } - | - `spike` dropped here while still borrowed -``` - -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 evil_feeder(input: &mut T, val: T) { - *input = val; -} -``` - -All it does is 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. - -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`. 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),