Skip to content

Commit 9c17e30

Browse files
conradludgateehuss
authored andcommitted
continue
1 parent ae69217 commit 9c17e30

File tree

1 file changed

+87
-149
lines changed

1 file changed

+87
-149
lines changed

src/subtyping.md

Lines changed: 87 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,41 @@
22

33
Rust uses lifetimes to track the relationships between borrows and ownership.
44
However, a naive implementation of lifetimes would be either too restrictive,
5-
or permit undefined behaviour. Let's see a few examples:
5+
or permit undefined behavior.
66

7-
```rust,ignore
8-
fn debug<'a>(a: &'a str, b: &'a str) {
9-
println!("a = {:?} b = {:?}", a, b)
7+
In order to allow flexible usage of lifetimes
8+
while also preventing mis-use, Rust uses a combination of **Subtyping** and **Variance**.
9+
10+
## Subtyping
11+
12+
Subtyping is the idea that one type can be a *subtype* of another.
13+
Let's define that `A: B` is equivalent to saying '`A` is a subtype of `B`'.
14+
What this is suggesting to us is that the set of *requirements* that `B` defines
15+
are completely satisfied by `A`. `A` may then have more requirements.
16+
17+
An example of simple subtyping that exists in the language are [supertraits](https://doc.rust-lang.org/stable/book/ch19-03-advanced-traits.html?highlight=supertraits#using-supertraits-to-require-one-traits-functionality-within-another-trait)
18+
19+
```rust
20+
use std::fmt;
21+
22+
trait OutlinePrint: fmt::Display {
23+
fn outline_print(&self) {
24+
todo!()
25+
}
26+
}
27+
```
28+
29+
Here, we have that `OutlinePrint: fmt::Display` (`OutlinePrint` is a *subtype* of `Display`),
30+
because it has all the requirements of `fmt::Display`, plus the `outline_print` function.
31+
32+
However, subtyping in traits is not that interesting in the case of Rust.
33+
Here in the nomicon, we're going to focus more with how subtyping interacts with **lifetimes**
34+
35+
Take this example
36+
37+
```rust
38+
fn debug<T: std::fmt::Debug>(a: T, b: T) {
39+
println!("a = {:?} b = {:?}", a, b);
1040
}
1141

1242
fn main() {
@@ -24,163 +54,73 @@ we might see the following error:
2454

2555
```text
2656
error[E0308]: mismatched types
27-
--> src/main.rs:6:16
28-
|
29-
6 | debug(a, b);
30-
| ^
31-
| |
32-
| expected `&'static str`, found struct `&'b str`
57+
--> src/main.rs:10:16
58+
|
59+
10 | debug(a, b);
60+
| ^
61+
| |
62+
| expected `&'static str`, found struct `&'b str`
3363
```
3464

35-
This is over-restrictive. In this case, what we want is to accept any type that lives "at least as long" as `<'a>`.
36-
This is what subtyping is intended to fix.
37-
38-
Let's define lifetime `'a` to be a `subtype` of lifetime `'b`, if and only if `'a` lives _at least as long_ as `'b`.
39-
We will denote this as `'a: 'b`
40-
41-
---
65+
This is over-restrictive. In this case, what we want is to accept any type that lives *at least as long* as `'b`.
66+
Let's try using subtyping with our lifetimes.
4267

43-
Subtyping is a relationship between types that allows statically typed
44-
languages to be a bit more flexible and permissive.
68+
Let's define a lifetime to have the a simple set of requirements: `'a` defines a region of code in which a value will be alive.
69+
Now that we have a defined set of requirements for lifetimes, we can define how they relate to each other.
70+
`'a: 'b` if and only if `'a` defines a region of code that **completely contains** `'b`.
4571

46-
Subtyping in Rust is a bit different from subtyping in other languages. This
47-
makes it harder to give simple examples, which is a problem since subtyping,
48-
and especially variance, is already hard to understand properly. As in,
49-
even compiler writers mess it up all the time.
72+
`'a` may define a region larger than `'b`, but that still fits our definition.
73+
Going back to our example above, we can say that `'static: 'b`.
5074

51-
To keep things simple, this section will consider a small extension to the
52-
Rust language that adds a new and simpler subtyping relationship. After
53-
establishing concepts and issues under this simpler system,
54-
we will then relate it back to how subtyping actually occurs in Rust.
55-
56-
So here's our simple extension, *Objective Rust*, featuring three new types:
75+
For now, let's accept the idea that subtypes of lifetimes can be transitive (more on this in [Variance](#variance>)),
76+
eg. `&'static str` is a subtype of `&'b str`, then we can let them coerce, and then the example above will compile
5777

5878
```rust
59-
trait Animal {
60-
fn snuggle(&self);
61-
fn eat(&mut self);
62-
}
63-
64-
trait Cat: Animal {
65-
fn meow(&self);
79+
fn debug<T: std::fmt::Debug>(a: T, b: T) {
80+
println!("a = {:?} b = {:?}", a, b);
6681
}
6782

68-
trait Dog: Animal {
69-
fn bark(&self);
70-
}
71-
```
72-
73-
But unlike normal traits, we can use them as concrete and sized types, just like structs.
74-
75-
Now, say we have a very simple function that takes an Animal, like this:
76-
77-
<!-- ignore: simplified code -->
78-
```rust,ignore
79-
fn love(pet: Animal) {
80-
pet.snuggle();
83+
fn main() {
84+
let a: &'static str = "hello";
85+
{
86+
let b = String::from("world");
87+
let b = &b; // 'b has a shorter lifetime than 'static
88+
debug(a, b); // a silently converts from `&'static str` into `&'b str`
89+
}
8190
}
8291
```
8392

84-
By default, static types must match *exactly* for a program to compile. As such,
85-
this code won't compile:
86-
87-
<!-- ignore: simplified code -->
88-
```rust,ignore
89-
let mr_snuggles: Cat = ...;
90-
love(mr_snuggles); // ERROR: expected Animal, found Cat
91-
```
92-
93-
Mr. Snuggles is a Cat, and Cats aren't *exactly* Animals, so we can't love him! 😿
94-
95-
This is annoying because Cats *are* Animals. They support every operation
96-
an Animal supports, so intuitively `love` shouldn't care if we pass it a `Cat`.
97-
We should be able to just **forget** the non-animal parts of our `Cat`, as they
98-
aren't necessary to love it.
99-
100-
This is exactly the problem that *subtyping* is intended to fix. Because Cats are just
101-
Animals **and more**, we say Cat is a *subtype* of Animal (because Cats are a *subset*
102-
of all the Animals). Equivalently, we say that Animal is a *supertype* of Cat.
103-
With subtypes, we can tweak our overly strict static type system
104-
with a simple rule: anywhere a value of type `T` is expected, we will also
105-
accept values that are subtypes of `T`.
106-
107-
Or more concretely: anywhere an Animal is expected, a Cat or Dog will also work.
108-
109-
As we will see throughout the rest of this section, subtyping is a lot more complicated
110-
and subtle than this, but this simple rule is a very good 99% intuition. And unless you
111-
write unsafe code, the compiler will automatically handle all the corner cases for you.
112-
113-
But this is the Rustonomicon. We're writing unsafe code, so we need to understand how
114-
this stuff really works, and how we can mess it up.
115-
116-
The core problem is that this rule, naively applied, will lead to *meowing Dogs*. That is,
117-
we can convince someone that a Dog is actually a Cat. This completely destroys the fabric
118-
of our static type system, making it worse than useless (and leading to Undefined Behavior).
119-
120-
Here's a simple example of this happening when we apply subtyping in a completely naive
121-
"find and replace" way.
93+
## Variance
12294

123-
<!-- ignore: simplified code -->
124-
```rust,ignore
125-
fn evil_feeder(pet: &mut Animal) {
126-
let spike: Dog = ...;
95+
Above, we glossed over the fact that `'static: 'b` implied that `&'static T: &'b T`. This uses a property known as variance.
96+
It's not always as simple as this example though, to understand that let's try extend this example a bit
12797

128-
// `pet` is an Animal, and Dog is a subtype of Animal,
129-
// so this should be fine, right..?
130-
*pet = spike;
98+
```rust,compile_fail
99+
fn debug<T>(a: &mut T, b: T) {
100+
*a = b;
131101
}
132102
133103
fn main() {
134-
let mut mr_snuggles: Cat = ...;
135-
evil_feeder(&mut mr_snuggles); // Replaces mr_snuggles with a Dog
136-
mr_snuggles.meow(); // OH NO, MEOWING DOG!
104+
let mut a: &'static str = "hello";
105+
{
106+
let b = String::from("world");
107+
let b = &b;
108+
debug(&mut a, b);
109+
}
137110
}
138111
```
139112

140-
Clearly, we need a more robust system than "find and replace". That system is *variance*,
141-
which is a set of rules governing how subtyping should compose. Most importantly, variance
142-
defines situations where subtyping should be disabled.
113+
This has a memory bug in it.
143114

144-
But before we get into variance, let's take a quick peek at where subtyping actually occurs in
145-
Rust: *lifetimes*!
146-
147-
> NOTE: The typed-ness of lifetimes is a fairly arbitrary construct that some
148-
> disagree with. However it simplifies our analysis to treat lifetimes
149-
> and types uniformly.
150-
151-
Lifetimes are just regions of code, and regions can be partially ordered with the *contains*
152-
(outlives) relationship. Subtyping on lifetimes is in terms of that relationship:
153-
if `'big: 'small` ("big contains small" or "big outlives small"), then `'big` is a subtype
154-
of `'small`. This is a large source of confusion, because it seems backwards
155-
to many: the bigger region is a *subtype* of the smaller region. But it makes
156-
sense if you consider our Animal example: Cat is an Animal *and more*,
157-
just as `'big` is `'small` *and more*.
158-
159-
Put another way, if someone wants a reference that lives for `'small`,
160-
usually what they actually mean is that they want a reference that lives
161-
for *at least* `'small`. They don't actually care if the lifetimes match
162-
exactly. So it should be ok for us to **forget** that something lives for
163-
`'big` and only remember that it lives for `'small`.
164-
165-
The meowing dog problem for lifetimes will result in us being able to
166-
store a short-lived reference in a place that expects a longer-lived one,
167-
creating a dangling reference and letting us use-after-free.
168-
169-
It will be useful to note that `'static`, the forever lifetime, is a subtype of
170-
every lifetime because by definition it outlives everything. We will be using
171-
this relationship in later examples to keep them as simple as possible.
172-
173-
With all that said, we still have no idea how to actually *use* subtyping of lifetimes,
174-
because nothing ever has type `'a`. Lifetimes only occur as part of some larger type
175-
like `&'a u32` or `IterMut<'a, u32>`. To apply lifetime subtyping, we need to know
176-
how to compose subtyping. Once again, we need *variance*.
177-
178-
## Variance
115+
If we were to expand this out, we'd see that we're trying to assign a `&'b str` into a `&'static str`,
116+
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.
179117

180-
Variance is where things get a bit complicated.
118+
However, the implementation of `debug` is valid.
119+
Therefore, this must mean that `&mut &'static str` should **not** a *subtype* of `&mut &'b str`,
120+
even if `'static` is a subtype of `'b`.
181121

182-
Variance is a property that *type constructors* have with respect to their
183-
arguments. A type constructor in Rust is any generic type with unbound arguments.
122+
Variance is the way that Rust defines the transitivity of subtypes through their *type constructor*.
123+
A type constructor in Rust is any generic type with unbound arguments.
184124
For instance `Vec` is a type constructor that takes a type `T` and returns
185125
`Vec<T>`. `&` and `&mut` are type constructors that take two inputs: a
186126
lifetime, and a type to point to.
@@ -192,20 +132,18 @@ A type constructor F's *variance* is how the subtyping of its inputs affects the
192132
subtyping of its outputs. There are three kinds of variance in Rust. Given two
193133
types `Sub` and `Super`, where `Sub` is a subtype of `Super`:
194134

195-
* `F` is *covariant* if `F<Sub>` is a subtype of `F<Super>` (subtyping "passes through")
196-
* `F` is *contravariant* if `F<Super>` is a subtype of `F<Sub>` (subtyping is "inverted")
197-
* `F` is *invariant* otherwise (no subtyping relationship exists)
135+
* F is **covariant** if `F<Sub>` is a subtype of `F<Super>` (the subtype property is passed through)
136+
* F is **contravariant** if `F<Super>` is a subtype of `F<Sub>` (the subtype property is "inverted")
137+
* F is **invariant** otherwise (no subtyping relationship exists)
198138

199-
If `F` has multiple type parameters, we can talk about the individual variances
200-
by saying that, for example, `F<T, U>` is covariant over `T` and invariant over `U`.
139+
If we remember from the above examples,
140+
it was ok for us to treat `&'a T` as a subtype of `&'b T` if `'a: 'b`,
141+
therefore we can say that `&'a T` is *covariant* over `'a`.
201142

202-
It is very useful to keep in mind that covariance is, in practical terms, "the"
203-
variance. Almost all consideration of variance is in terms of whether something
204-
should be covariant or invariant. Actually witnessing contravariance is quite difficult
205-
in Rust, though it does in fact exist.
143+
Also, we saw that it was not ok for us to treat `&mut &'a T` as a subtype of `&mut &'b T`,
144+
therefore we can say that `&mut T` is *invariant* over `T`
206145

207-
Here is a table of important variances which the rest of this section will be devoted
208-
to trying to explain:
146+
Here is a table of some other type constructors and their variances:
209147

210148
| | | 'a | T | U |
211149
|---|-----------------|:---------:|:-----------------:|:---------:|

0 commit comments

Comments
 (0)