First with fn set<'a>(self: &'a MyCell<T>, value: T)
. Methods are a bit harder to write signatures for, let’s see the equivalent free-standing function signature
fn set_immut<'a, T: 'a>(this: &'a MyCell<T>, value: T)
Also, we already know that we’re only using the case where T
is some &i32
type, so let’s reduce the generality a bit:
fn set_immut<'a, 'b: 'a>(this: &'a MyCell<&'b i32>, value: &'b i32)
The same analysis would give us a signature of the mutable case of
fn set_mut<'a, 'b: 'a>(this: &'a mut MyCell<&'b i32>, value: &'b i32)
by the way.
Now, why does the code
fn foo<'c>(mut cell: MyCell<&'c i32>) -> MyCell<&'c i32> {
let val: i32 = 13;
set_immut(&cell, &val);
cell
}
compile?
Let’s call the “lifetime” of how long the variable val
exists and can be borrowed 'val
. Then &val
is of some type &'d i32
with 'val: 'd
. (These bounds are “outlives” bounds, i.e. the lifetime 'val
is longer or equal than 'd
.)
This is passed to set_immut
. As well as &cell
. This borrow of cell
is for some lifetime 'a
which is not really too relevant for the issue at hand, so let’s not discuss it further; and it’s a borrow of a value of type MyCell<&'c i32>
.
So the two values passed to set_immut
are of type &'a MyCell<'c i32>
and of type &'d i32
. We call set_immut<'a, 'b>
– as I’ve said we aren’t worried about 'a
, so I already gave them a matching name, but what is 'b
?
This is where variance comes in. Looking back at the signature
fn set_immut<'a, 'b: 'a>(this: &'a MyCell<&'b i32>, value: &'b i32)
the first argument is of expected type &'a MyCell<&'b i32>
, but its actual type is &'a MyCell<&'c i32>
. How can we make this match? The compiler will introduce an implicit subtyping coercion! In this case, &'a MyCell<&'c i32>
is a type that is covariant in both lifetime arguments, in particular it’s covariant in 'c
. So the coercion can convert our type as desired as long as 'c: 'b
.
Similarly, for the second argument, we coerce &'d i32
into &'b i32
successfully as long as 'd: 'b
.
The resulting complete list of lifetime constraint we gathered:
'val: 'd
'd: 'b
'c: 'b
These bounds can be trivially fulfilled by making 'b
a really short lifetime, any by making 'd
any lifetime between 'val
and 'b
. These two lifetimes we (the compiler) are completely free to choose however we want to fulfill these constraints (unlike 'c
which is the parameter of our function and 'val
which we defined to denote the time the variable val
exists / can be borrowed for).
Now what changes with &mut
?
fn foo<'c>(mut cell: MyCell<&'c i32>) -> MyCell<&'c i32> {
let val: i32 = 13;
set_mut(&mut cell, &val);
cell
}
The analysis is quite similar: the two values passed to set_mut
are of type &'a mut MyCell<'c i32>
and of type &'d i32
, with 'd
being a lifetime 'val: 'd
as before.
However the type &'a mut MyCell<'c i32>
is covariant in 'a
(a fact we don’t care much about, as we don’t look at 'a
here), and invariant in 'c
. This means that our implicit subtyping coercion cannot change the lifetime 'c
, and 'c == 'b
must be the same lifetime. Lifetime equality can also be written as a combination of two bounds: 'c: 'b
and 'b: 'c
.
The resulting complete list of lifetime constraint we gathered this time is:
'val: 'd
'd: 'b
'c: 'b
'b: 'c // this one is new, compared to the immutable case
This list of constraints can not be fulfilled. There is the outlives relation
'val: 'd: 'b: 'c
// which transitively implies the condition
'val: 'c
which means that the variable val
must live longer than the lifetime 'c
. But 'c
is a caller-provided lifetime, external to our function. And this is why the compiler will complain: `val` does not live long enough
, with the hint argument requires that `val` is borrowed for `'c`
.