From 931728a825b1b2ea4b58e695b64eb86f951464fb Mon Sep 17 00:00:00 2001 From: Alexis Beingessner Date: Fri, 26 Jun 2015 16:52:20 -0700 Subject: [PATCH] niko fixes --- README.md | 1 - lifetimes.md | 221 +++++++++++++++++++++++++++++---------------------- 2 files changed, 126 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 19fd3f2..04cfe58 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,6 @@ quite permisive with respect to other dubious operations. Rust considers it * Deadlock * Leak memory * Fail to call destructors -* Access private fields * Overflow integers * Delete the production database diff --git a/lifetimes.md b/lifetimes.md index fe4dc62..48bfcb0 100644 --- a/lifetimes.md +++ b/lifetimes.md @@ -102,59 +102,11 @@ more than a local lint against incorrect usage of a value. -# Weird Lifetimes - -Given the following code: - -```rust -struct Foo; - -impl Foo { - fn mutate_and_share(&mut self) -> &Self { &*self } - fn share(&self) {} -} - -fn main() { - let mut foo = Foo; - let loan = foo.mutate_and_share(); - foo.share(); -} -``` - -One might expect it to compile. We call `mutate_and_share`, which mutably borrows -`foo` *temporarily*, but then returns *only* a shared reference. Therefore we -would expect `foo.share()` to succeed as `foo` shouldn't be mutably borrowed. - -However when we try to compile it: - -```text -:11:5: 11:8 error: cannot borrow `foo` as immutable because it is also borrowed as mutable -:11 foo.share(); - ^~~ -:10:16: 10:19 note: previous borrow of `foo` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `foo` until the borrow ends -:10 let loan = foo.mutate_and_share(); - ^~~ -:12:2: 12:2 note: previous borrow ends here -:8 fn main() { -:9 let mut foo = Foo; -:10 let loan = foo.mutate_and_share(); -:11 foo.share(); -:12 } - ^ -``` - -What happened? Well, the lifetime of `loan` is derived from a *mutable* borrow. -This makes the type system believe that `foo` is mutably borrowed as long as -`loan` exists, even though it's a shared reference. To my knowledge, this is not -a bug. - - - # Lifetime Elision In order to make common patterns more ergonomic, Rust allows lifetimes to be -*elided* in function, impl, and type signatures. +*elided* in function signatures. A *lifetime position* is anywhere you can write a lifetime in a type: @@ -336,16 +288,18 @@ In Rust, subtyping derives entirely from *lifetimes*. Since lifetimes are derive from scopes, we can partially order them based on an *outlives* relationship. We can even express this as a generic bound: `T: 'a` specifies that `T` *outlives* `'a`. -We can then define subtyping on lifetimes in terms of lifetimes: `'a : 'b` implies -`'a <: b` -- if `'a` outlives `'b`, then `'a` is a subtype of `'b`. This is a very +We can then define subtyping on lifetimes in terms of lifetimes: if `'a : 'b` +("a outlives b"), then `'a` is a subtype of `b`. This is a large source of confusion, because a bigger scope is a *sub type* of a smaller scope. This does in fact make sense. The intuitive reason for this is that if you expect an -`&'a u8`, then it's totally fine for me to hand you an `&'static u8`, in the same way +`&'a u8`, then it's totally fine for me to hand you an `&'static u8` in the same way that if you expect an Animal in Java, it's totally fine for me to hand you a Cat. (Note, the subtyping relationship and typed-ness of lifetimes is a fairly arbitrary construct that some disagree with. I just find that it simplifies this analysis.) +TODO: higher rank lifetime subtyping + Variance is where things get really harsh. Variance is a property that *type constructors* have. A type constructor in Rust @@ -356,21 +310,27 @@ take a lifetime and a type. A type constructor's *variance* is how the subtypes of its inputs affects the subtypes of its outputs. There are three kinds of variance: -* F is *covariant* if `T <: U` implies `F <: F` -* F is *contravariant* if `T <: U` implies `F <: F` +* F is *variant* if `T` being a subtype of `U` implies `F` is a subtype of `F` * F is *invariant* 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" variant is in fact *covariant*. Rust does not have contravariance. +Historically Rust did have some contravariance but it was scrapped due to poor +interactions with other features.) + Some important variances: -* `&` is covariant (as is *const by metaphor) +* `&` is variant (as is *const by metaphor) * `&mut` is invariant (as is *mut by metaphor) -* `Fn(T)` is contravariant with respect to `T` -* `Box`, `Vec`, and all other collections are covariant +* `Fn(T) -> U` is invariant with respect to `T`, but variant with respect to `U` +* `Box`, `Vec`, and all other collections are variant * `UnsafeCell`, `Cell`, `RefCell`, `Mutex` and all "interior mutability" types are invariant To understand why these variances are correct and desirable, we will consider several -examples. We have already covered why `&` should be covariant. +examples. We have already covered why `&` should be variant when introducing subtyping: +it's desirable to be able to pass longer-lived things where shorter-lived things are +needed. To see why `&mut` should be invariant, consider the following code: @@ -391,28 +351,29 @@ fn overwrite(input: &mut T, new: &mut T) { The signature of `overwrite` is clearly valid: it takes mutable references to two values of the same type, and replaces one with the other. We have seen already that `&` is -covariant, and `'static` is a subtype of *any* `'a`, so `&'static str` is a +variant, and `'static` is a subtype of *any* `'a`, so `&'static str` is a subtype of `&'a str`. Therefore, if `&mut` was -*also* covariant, then the lifetime of the `&'static str` would successfully be +*also* variant, then the lifetime of the `&'static str` would successfully be "shrunk" down to the shorter lifetime of the string, and `replace` would be called successfully. The 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 covariance vs -invariance: if covariance would allow you to *store* a short-lived value in a +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 in a longer-lived slot, then you must be invariant. -`Box` and `Vec` are interesting cases because they're covariant, but you can +`Box` and `Vec` are interesting cases because they're variant, but you can definitely store values in them! This is fine because *you can only store values in them through a mutable reference*! The mutable reference makes the whole type invariant, and therefore prevents you from getting in trouble. -Being covariant allows them to be covariant when shared immutably (so you can pass +Being variant allows them to be variant when shared immutably (so you can pass a `&Box<&'static str>` where a `&Box<&'a str>` is expected). It also allows you to forever weaken the type by moving it into a weaker slot. That is, you can do: ```rust fn get_box<'a>(&'a u8) -> Box<&'a str> { + // string literals are `&'static str`s Box::new("hello") } ``` @@ -424,51 +385,67 @@ The variance of the cell types similarly 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 confusing case, largely because contravariance is easily the -most confusing kind of variance, and basically never comes up. To understand it, -consider a function `len` that takes a function `F`. +`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: ```rust -fn len(func: F) -> usize - where F: Fn(&'static str) -> usize -{ - func("hello") -} +// 'a is derived from some parent scope +fn foo(&'a str) -> usize; ``` -We require that F is a Fn that can take an `&'static str` and returns a usize. Now -say we have a function that can take an `&'a str` (for *some* `'a`). Such a function actually -accepts *more* inputs, since `&'static str` is a subtype of `&'a str`. Therefore -`len` should happily accept such a function! +This signature claims that it can handle any &str that lives *at least* as long +as `'a`. Now if this signature was variant with respect to &str, that would mean + +```rust +fn foo(&'static str) -> usize; +``` -So a `Fn(&'a str)` is a subtype of a `Fn(&'static str)` because -`&'static str` is a subtype of `&'a str`. Exactly contravariance. +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. Therefore functions are not variant over their arguments. -The variance of `*const` and `*mut` is basically arbitrary as they're not at all -type or memory safe, so their variance is determined in analogy to & and &mut -respectively. +To see why `Fn(T) -> U` should be *variant* over U, consider the following +function signature: + +```rust +// 'a is derived from some parent scope +fn foo(usize) -> &'a str; +``` + +This signature claims that it will return something that outlives `'a`. It is +therefore completely reasonable to provide + +```rust +fn foo(usize) -> &'static str; +``` + +in its place. Therefore functions *are* variant 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 +as invariant in analogy to cells. 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` +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. -* 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 +* If all uses of A are variant, then Foo is variant over A * Otherwise, Foo is invariant over A ```rust struct Foo<'a, 'b, A, B, C, D, E, F, G, H> { - a: &'a A, // covariant over 'a and A + a: &'a A, // variant over 'a and A b: &'b mut B, // invariant over 'b and B - c: *const C, // covariant over C + c: *const C, // variant over C d: *mut D, // invariant over D - e: Vec, // covariant over E + e: Vec, // variant over E f: Cell, // invariant over F - g: G // covariant over G - h1: H // would also be covariant over H except... + g: G // variant over G + h1: H // would also be variant over H except... h2: Cell // invariant over H, because invariance wins } ``` @@ -497,8 +474,9 @@ correct variance and drop checking. We do this using *PhantomData*, which is a special marker type. PhantomData consumes no space, but simulates a field of the given type for the purpose of -variance. This was deemed to be less error-prone than explicitly telling the -type-system the kind of variance that you want. +static analysis. This was deemed to be less error-prone than explicitly telling +the type-system the kind of variance that you want, while also providing other +useful information. Iter logically contains `&'a T`, so this is exactly what we tell the PhantomData to simulate: @@ -526,16 +504,16 @@ However the one exception is with PhantomData. Given a struct like Vec: ``` struct Vec { - data: *const T, // *const for covariance! + data: *const T, // *const for variance! len: usize, cap: usize, } ``` -dropck will generously determine that Vec does not contain any values of +dropck will generously determine that Vec does not own any values of type T. This will unfortunately allow people to construct unsound Drop implementations that access data that has already been dropped. In order to -tell dropck that we *do* own values of type T and may call destructors of that +tell dropck that we *do* own values of type T, and may call destructors of that type, we must add extra PhantomData: ``` @@ -700,3 +678,56 @@ Bar is *also* live. Since IterMut is always live when `next` can be called, if to it exist! + + + +# Weird Lifetimes + +Given the following code: + +```rust +struct Foo; + +impl Foo { + fn mutate_and_share(&mut self) -> &Self { &*self } + fn share(&self) {} +} + +fn main() { + let mut foo = Foo; + let loan = foo.mutate_and_share(); + foo.share(); +} +``` + +One might expect it to compile. We call `mutate_and_share`, which mutably borrows +`foo` *temporarily*, but then returns *only* a shared reference. Therefore we +would expect `foo.share()` to succeed as `foo` shouldn't be mutably borrowed. + +However when we try to compile it: + +```text +:11:5: 11:8 error: cannot borrow `foo` as immutable because it is also borrowed as mutable +:11 foo.share(); + ^~~ +:10:16: 10:19 note: previous borrow of `foo` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `foo` until the borrow ends +:10 let loan = foo.mutate_and_share(); + ^~~ +:12:2: 12:2 note: previous borrow ends here +:8 fn main() { +:9 let mut foo = Foo; +:10 let loan = foo.mutate_and_share(); +:11 foo.share(); +:12 } + ^ +``` + +What happened? Well, the lifetime of `loan` is derived from a *mutable* borrow. +This makes the type system believe that `foo` is mutably borrowed as long as +`loan` exists, even though it's a shared reference. This isn't a bug, although +one could argue it is a limitation of the design. In particular, to know if +the mutable part of the borrow is *really* expired we'd have to peek into +implementation details of the function. Currently, type-checking a function +does not need to inspect the bodies of any other functions or types. + +