Variance in practice
So why should you, as a Rust developer, care?
Many Rust developers start off by using reference counted smart pointers like
Rc
or Arc
instead of borrowed data everywhere. If you're doing that, you're
unlikely to run into lifetime issues. But you may eventually want to switch to
borrowed data to get maximum performance -- if so, you'll probably have to introduce
lifetime parameters into your code. That's when variance becomes important. Some
of the thorniest issues getting rustc to accept code with pervasive use of borrowed
data end up boiling down to variance in some fashion.
For example, consider this situation, extracted from some real-world Rust code:
#![allow(dead_code)] fn main() {} use std::collections::HashSet; use std::fmt; // Consider this struct representing a message. struct Message<'msg> { message: &'msg str, } // ... this struct that collects messages to be displayed. struct MessageCollector<'a, 'msg> { list: &'a mut Vec<Message<'msg>>, } impl<'a, 'msg> MessageCollector<'a, 'msg> { // This adds a message to the end of the list. fn add_message(&mut self, message: Message<'msg>) { self.list.push(message); } } // And this struct that displays collected messages. struct MessageDisplayer<'a, 'msg> { list: &'a Vec<Message<'msg>>, } impl<'a, 'msg> fmt::Display for MessageDisplayer<'a, 'msg> { // This displays all the messages, separated by newlines. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for message in self.list { write!(f, "{}\n", message.message)?; } Ok(()) } } fn message_example() { // Here's a simple pool of messages. let mut message_pool: HashSet<String> = HashSet::new(); message_pool.insert("ten".to_owned()); message_pool.insert("twenty".to_owned()); // All right, let's try collecting and displaying some messages! collect_and_display(&message_pool); } fn collect_and_display<'msg>(message_pool: &'msg HashSet<String>) { let mut list = vec![]; // Collect some messages. (This is pretty simple but you can imagine the // collector being passed into other code.) let mut collector = MessageCollector { list: &mut list }; for message in message_pool { collector.add_message(Message { message }); } // Now let's display those messages! let displayer = MessageDisplayer { list: &list }; println!("{}", displayer); }
This works, but can it be simplified?
Let's try reducing the number of lifetime parameters, first for the displayer.
#![allow(dead_code)] fn main() {} use std::collections::HashSet; use std::fmt; struct Message<'msg> { message: &'msg str, } struct MessageCollector<'a, 'msg> { list: &'a mut Vec<Message<'msg>>, } impl<'a, 'msg> MessageCollector<'a, 'msg> { fn add_message(&mut self, message: Message<'msg>) { self.list.push(message); } } struct SimpleMessageDisplayer<'a> { list: &'a Vec<Message<'a>>, } impl<'a> fmt::Display for SimpleMessageDisplayer<'a> { // This displays all the messages. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for message in self.list { write!(f, "{}\n", message.message)?; } Ok(()) } } fn collect_and_display_2<'msg>(message_pool: &'msg HashSet<String>) { // OK, let's do the same thing as collect_and_display, except using the // simple displayer. let mut list = vec![]; // Collect some messages. let mut collector = MessageCollector { list: &mut list }; for message in message_pool { collector.add_message(Message { message }); } // Finally, display them. let displayer = SimpleMessageDisplayer { list: &list }; println!("{}", displayer); }
OK, that worked. Can we do the same for the collector? Let's try it out:
#![allow(dead_code)] fn main() {} use std::collections::HashSet; use std::fmt; struct Message<'msg> { message: &'msg str, } struct MessageCollector<'a, 'msg> { list: &'a mut Vec<Message<'msg>>, } impl<'a, 'msg> MessageCollector<'a, 'msg> { fn add_message(&mut self, message: Message<'msg>) { self.list.push(message); } } struct SimpleMessageDisplayer<'a> { list: &'a Vec<Message<'a>>, } impl<'a> fmt::Display for SimpleMessageDisplayer<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for message in self.list { write!(f, "{}\n", message.message)?; } Ok(()) } } struct SimpleMessageCollector<'a> { list: &'a mut Vec<Message<'a>>, } impl<'a> SimpleMessageCollector<'a> { // This adds a message to the end of the list. fn add_message(&mut self, message: Message<'a>) { self.list.push(message); } } fn collect_and_display_3<'msg>(message_pool: &'msg HashSet<String>) { // OK, one more time. let mut list = vec![]; // Collect some messages. let mut collector = SimpleMessageCollector { list: &mut list }; for message in message_pool { collector.add_message(Message { message }); } // Finally, display them. let displayer = SimpleMessageDisplayer { list: &list }; println!("{}", displayer); }
That doesn't work! rustc (as of 1.43.1) errors out with cannot borrow `list` as immutable because it is also borrowed as mutable
.
Why did reducing the number of lifetime params work for MessageDisplayer
but not
MessageCollector
? It's all because of variance. Let's have a look at the structs
again, first the displayer:
#![allow(dead_code)]
fn main() {}
struct Message<'msg> {
message: &'msg str,
}
struct MessageDisplayer<'a, 'msg> {
// Two lifetime parameters:
list: &'a Vec<Message<'msg>>,
// Here, the compiler can vary the two independently, so the list can be
// held onto a shorter lifetime than 'msg, then released.
}
struct SimpleMessageDisplayer<'a> {
// 'a is used in two spots:
//
// | |
// v v
list: &'a Vec<Message<'a>>,
//
// But since both of them are covariant (in immutable positions), 'a is
// covariant as well. This means that the compiler can internally transform
// &'a Vec<Message<'msg>> into the shorter &'a Vec<Message<'a>>, and hold the
// list for the shorter 'a duration.
}
struct MessageCollector<'a, 'msg> {
// Two lifetime parameters, again:
list: &'a mut Vec<Message<'msg>>,
// Here, 'a is covariant, but 'msg is invariant since it is "inside"
// a &mut reference. The compiler can vary the two independently, which
// means that the list can be held onto for a shorter lifetime than 'msg.
}
struct SimpleMessageCollector<'a> {
// 'a is used in two spots again:
//
// | |
// v v
list: &'a mut Vec<Message<'a>>,
//
// The first 'a is covariant, but the second one is invariant since it is
// "inside" a &mut reference! This means that 'a is invariant, and this
// ends up causing the compiler to try and hold on to the list for longer
// than with the standard MessageCollector.
}
The simple version:
#![allow(dead_code)]
fn main() {}
struct Message<'msg> {
message: &'msg str,
}
struct MessageDisplayer<'a, 'msg> {
// Two lifetime parameters:
list: &'a Vec<Message<'msg>>,
// Here, the compiler can vary the two independently, so the list can be
// held onto a shorter lifetime than 'msg, then released.
}
struct SimpleMessageDisplayer<'a> {
// 'a is used in two spots:
//
// | |
// v v
list: &'a Vec<Message<'a>>,
//
// But since both of them are covariant (in immutable positions), 'a is
// covariant as well. This means that the compiler can internally transform
// &'a Vec<Message<'msg>> into the shorter &'a Vec<Message<'a>>, and hold the
// list for the shorter 'a duration.
}
struct MessageCollector<'a, 'msg> {
// Two lifetime parameters, again:
list: &'a mut Vec<Message<'msg>>,
// Here, 'a is covariant, but 'msg is invariant since it is "inside"
// a &mut reference. The compiler can vary the two independently, which
// means that the list can be held onto for a shorter lifetime than 'msg.
}
struct SimpleMessageCollector<'a> {
// 'a is used in two spots again:
//
// | |
// v v
list: &'a mut Vec<Message<'a>>,
//
// The first 'a is covariant, but the second one is invariant since it is
// "inside" a &mut reference! This means that 'a is invariant, and this
// ends up causing the compiler to try and hold on to the list for longer
// than with the standard MessageCollector.
}
Now the collector:
#![allow(dead_code)]
fn main() {}
struct Message<'msg> {
message: &'msg str,
}
struct MessageDisplayer<'a, 'msg> {
// Two lifetime parameters:
list: &'a Vec<Message<'msg>>,
// Here, the compiler can vary the two independently, so the list can be
// held onto a shorter lifetime than 'msg, then released.
}
struct SimpleMessageDisplayer<'a> {
// 'a is used in two spots:
//
// | |
// v v
list: &'a Vec<Message<'a>>,
//
// But since both of them are covariant (in immutable positions), 'a is
// covariant as well. This means that the compiler can internally transform
// &'a Vec<Message<'msg>> into the shorter &'a Vec<Message<'a>>, and hold the
// list for the shorter 'a duration.
}
struct MessageCollector<'a, 'msg> {
// Two lifetime parameters, again:
list: &'a mut Vec<Message<'msg>>,
// Here, 'a is covariant, but 'msg is invariant since it is "inside"
// a &mut reference. The compiler can vary the two independently, which
// means that the list can be held onto for a shorter lifetime than 'msg.
}
struct SimpleMessageCollector<'a> {
// 'a is used in two spots again:
//
// | |
// v v
list: &'a mut Vec<Message<'a>>,
//
// The first 'a is covariant, but the second one is invariant since it is
// "inside" a &mut reference! This means that 'a is invariant, and this
// ends up causing the compiler to try and hold on to the list for longer
// than with the standard MessageCollector.
}
Finally, the problematic simple version:
#![allow(dead_code)]
fn main() {}
struct Message<'msg> {
message: &'msg str,
}
struct MessageDisplayer<'a, 'msg> {
// Two lifetime parameters:
list: &'a Vec<Message<'msg>>,
// Here, the compiler can vary the two independently, so the list can be
// held onto a shorter lifetime than 'msg, then released.
}
struct SimpleMessageDisplayer<'a> {
// 'a is used in two spots:
//
// | |
// v v
list: &'a Vec<Message<'a>>,
//
// But since both of them are covariant (in immutable positions), 'a is
// covariant as well. This means that the compiler can internally transform
// &'a Vec<Message<'msg>> into the shorter &'a Vec<Message<'a>>, and hold the
// list for the shorter 'a duration.
}
struct MessageCollector<'a, 'msg> {
// Two lifetime parameters, again:
list: &'a mut Vec<Message<'msg>>,
// Here, 'a is covariant, but 'msg is invariant since it is "inside"
// a &mut reference. The compiler can vary the two independently, which
// means that the list can be held onto for a shorter lifetime than 'msg.
}
struct SimpleMessageCollector<'a> {
// 'a is used in two spots again:
//
// | |
// v v
list: &'a mut Vec<Message<'a>>,
//
// The first 'a is covariant, but the second one is invariant since it is
// "inside" a &mut reference! This means that 'a is invariant, and this
// ends up causing the compiler to try and hold on to the list for longer
// than with the standard MessageCollector.
}
A final note if you're writing a Rust library
Changing the variance of a parameter (lifetime or type) from covariant to anything else, or from contravariant to anything else, is a BREAKING CHANGE. If you're following semver, it can only be done with a new major version.
Changing a parameter from invariant to co- or contravariant is not a breaking change.