pull/102/head
Alexis Beingessner 6 years ago
parent b28d364f0f
commit 6ec81e17f6

@ -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<X>`. `&` 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<T>`. `&` 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<T>` 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<T, U>` 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`

Loading…
Cancel
Save