|
| 1 | +--- |
| 2 | +minutes: 30 |
| 3 | +--- |
| 4 | + |
| 5 | +# `PhantomData` and Lifetime Subtyping (Branding 2/4) |
| 6 | + |
| 7 | +Idea: |
| 8 | + |
| 9 | +- Use a lifetime as a unique brand for each token. |
| 10 | +- Make lifetimes sufficiently distinct so that they don't implicitly convert |
| 11 | + into each other. |
| 12 | + |
| 13 | +<!-- dprint-ignore-start --> |
| 14 | +```rust,editable |
| 15 | +use std::marker::PhantomData; |
| 16 | +
|
| 17 | +#[derive(Default)] |
| 18 | +struct InvariantLifetime<'id>(PhantomData<&'id ()>); // The main focus |
| 19 | +
|
| 20 | +struct Wrapper<'a> { value: u8, invariant: InvariantLifetime<'a> } |
| 21 | +
|
| 22 | +fn lifetime_separator<T>(value: u8, f: impl for<'a> FnOnce(Wrapper<'a>) -> T) -> T { |
| 23 | + f(Wrapper { value, invariant: InvariantLifetime::default() }) |
| 24 | +} |
| 25 | +
|
| 26 | +fn try_coerce_lifetimes<'a>(left: Wrapper<'a>, right: Wrapper<'a>) {} |
| 27 | +
|
| 28 | +fn main() { |
| 29 | + lifetime_separator(1, |wrapped_1| { |
| 30 | + lifetime_separator(2, |wrapped_2| { |
| 31 | + // We want this to NOT compile |
| 32 | + try_coerce_lifetimes(wrapped_1, wrapped_2); |
| 33 | + }); |
| 34 | + }); |
| 35 | +} |
| 36 | +``` |
| 37 | +<!-- dprint-ignore-end --> |
| 38 | + |
| 39 | +<details> |
| 40 | + |
| 41 | +<!-- TODO: Link back to PhantomData in the borrowck invariants chapter. |
| 42 | +- We saw `PhantomData` back in the Borrow Checker Invariants chapter. |
| 43 | +--> |
| 44 | + |
| 45 | +- In Rust, lifetimes can have subtyping relations between one another. |
| 46 | + |
| 47 | + This kind of relation allows the compiler to determine if one lifetime |
| 48 | + outlives another. |
| 49 | + |
| 50 | + Determining if a lifetime outlives another also allows us to say _the shortest |
| 51 | + common lifetime is the one that ends first_. |
| 52 | + |
| 53 | + This is useful in many cases, as it means two different lifetimes can be |
| 54 | + treated as if they were the same in the regions they do overlap. |
| 55 | + |
| 56 | + This is usually what we want. But here we want to use lifetimes as a way to |
| 57 | + distinguish values so we say that a token only applies to a single variable |
| 58 | + without having to create a newtype for every single variable we declare. |
| 59 | + |
| 60 | +- **Goal**: We want two lifetimes that the rust compiler cannot determine if one |
| 61 | + outlives the other. |
| 62 | + |
| 63 | + We are using `try_coerce_lifetimes` as a compile-time check to see if the |
| 64 | + lifetimes have a common shorter lifetime (AKA being subtyped). |
| 65 | + |
| 66 | +- Note: This slide compiles, by the end of this slide it should only compile |
| 67 | + when `subtyped_lifetimes` is commented out. |
| 68 | + |
| 69 | +- There are two important parts of this code: |
| 70 | + - The `impl for<'a>` bound on the closure passed to `lifetime_separator`. |
| 71 | + - The way lifetimes are used in the parameter for `PhantomData`. |
| 72 | + |
| 73 | +## `for<'a>` bound on a Closure |
| 74 | + |
| 75 | +- We are using `for<'a>` as a way of introducing a lifetime generic parameter to |
| 76 | + a function type and asking that the body of the function to work for all |
| 77 | + possible lifetimes. |
| 78 | + |
| 79 | + What this also does is remove some ability of the compiler to make assumptions |
| 80 | + about that specific lifetime for the function argument, as it must meet rust's |
| 81 | + borrow checking rules regardless of the "real" lifetime its arguments are |
| 82 | + going to have. The caller is substituting in actual lifetime, the function |
| 83 | + itself cannot. |
| 84 | + |
| 85 | + This is analogous to a forall (Ɐ) quantifier in mathematics, or the way we |
| 86 | + introduce `<T>` as type variables, but only for lifetimes in trait bounds. |
| 87 | + |
| 88 | + When we write a function generic over a type `T`, we can't determine that type |
| 89 | + from within the function itself. Even if we call a function |
| 90 | + `fn foo<T, U>(first: T, second: U)` with two arguments of the same type, the |
| 91 | + body of this function cannot determine if `T` and `U` are the same type. |
| 92 | + |
| 93 | + This also prevents _the API consumer_ from defining a lifetime themselves, |
| 94 | + which would allow them to circumvent the restrictions we want to impose. |
| 95 | + |
| 96 | +## PhantomData and Lifetime Variance |
| 97 | + |
| 98 | +- We already know `PhantomData`, which can introduce a formal no-op usage of an |
| 99 | + otherwise unused type or a lifetime parameter. |
| 100 | + |
| 101 | +- Ask: What can we do with `PhantomData`? |
| 102 | + |
| 103 | + Expect mentions of the Typestate pattern, tying together the lifetimes of |
| 104 | + owned values. |
| 105 | + |
| 106 | +- Ask: In other languages, what is subtyping? |
| 107 | + |
| 108 | + Expect mentions of inheritance, being able to use a value of type `B` when a |
| 109 | + asked for a value of type `A` because `B` is a "subtype" of `A`. |
| 110 | + |
| 111 | +- Rust does have Subtyping! But only for lifetimes. |
| 112 | + |
| 113 | + Ask: If one lifetime is a subtype of another lifetime, what might that mean? |
| 114 | + |
| 115 | + A lifetime is a "subtype" of another lifetime when it _outlives_ that other |
| 116 | + lifetime. |
| 117 | + |
| 118 | +- The way that lifetimes used by `PhantomData` behave depends not only on where |
| 119 | + the lifetime "comes from" but on how the reference is defined too. |
| 120 | + |
| 121 | + The reason this compiles is that the |
| 122 | + [**Variance**](https://doc.rust-lang.org/stable/reference/subtyping.html#r-subtyping.variance) |
| 123 | + of the lifetime inside of `InvariantLifetime` is too lenient. |
| 124 | + |
| 125 | + Note: Do not expect to get students to understand variance entirely here, just |
| 126 | + treat it as a kind of ladder of restrictiveness on the ability of lifetimes to |
| 127 | + establish subtyping relations. |
| 128 | + |
| 129 | + <!-- Note: We've been using "invariants" in this module in a specific way, but subtyping introduces _invariant_, _covariant_, and _contravariant_ as specific terms. --> |
| 130 | + |
| 131 | +- Ask: How can we make it more restrictive? How do we make a reference type more |
| 132 | + restrictive in rust? |
| 133 | + |
| 134 | + Expect or demonstrate: Making it `&'id mut ()` instead. This will not be |
| 135 | + enough! |
| 136 | + |
| 137 | + We need to use a |
| 138 | + [**Variance**](https://doc.rust-lang.org/stable/reference/subtyping.html#r-subtyping.variance) |
| 139 | + on lifetimes where subtyping cannot be inferred except on _identical |
| 140 | + lifetimes_. That is, the only subtype of `'a` the compiler can know is `'a` |
| 141 | + itself. |
| 142 | + |
| 143 | + Note: Again, do not try to get the whole class to understand variance. Treat |
| 144 | + it as a ladder of restrictiveness for now. |
| 145 | + |
| 146 | + Demonstrate: Move from `&'id ()` (covariant in lifetime and type), |
| 147 | + `&'id mut ()` (covariant in lifetime, invariant in type), `*mut &'id mut ()` |
| 148 | + (invariant in lifetime and type), and finally `*mut &'id ()` (invariant in |
| 149 | + lifetime but not type). |
| 150 | + |
| 151 | + Those last two should not compile, which means we've finally found candidates |
| 152 | + for how to bind lifetimes to `PhantomData` so they can't be compared to one |
| 153 | + another in this context. |
| 154 | + |
| 155 | + Reason: `*mut` means |
| 156 | + [mutable raw pointer](https://doc.rust-lang.org/reference/types/pointer.html#r-type.pointer.raw). |
| 157 | + Rust has mutable pointers! But you cannot reason about them in safe rust. |
| 158 | + Making this a mutable raw pointer to a reference that has a lifetime |
| 159 | + complicates the compiler's ability subtype because it cannot reason about |
| 160 | + mutable raw pointers within the borrow checker. |
| 161 | + |
| 162 | +- Wrap up: We've introduced ways to stop the compiler from deciding that |
| 163 | + lifetimes are "similar enough" by choosing a Variance for a lifetime in |
| 164 | + `PhantomData` that is restrictive enough to prevent this slide from compiling. |
| 165 | + |
| 166 | + That is, we can now create variables that can exist in the same scope as each |
| 167 | + other, but whose types are automatically made different from one another |
| 168 | + per-variable without much boilerplate. |
| 169 | + |
| 170 | +## More to Explore |
| 171 | + |
| 172 | +- The `for<'a>` quantifier is not just for function types. It is a |
| 173 | + [**Higher-ranked trait bound**](https://doc.rust-lang.org/reference/subtyping.html?search=Hiher#r-subtype.higher-ranked). |
| 174 | + |
| 175 | +</details> |
0 commit comments