I have a trait Employee
that contains a collection of associated types that represent a profession. For this example code, I added a Teacher
associated type along with its borrowed version BorrowedTeacher
. I have an enum OwnedEmployee<P: Employee>
of which each variant is one of the owned professions in the Employee
trait. Similarly, I have a EmployeeView
enum whose variants are the borrowed profession structs. I also have conversion functions to create the borrowed enum
from the owned one.
pub trait Employee: Clone + Copy {
type BT<'a>: BorrowedTeacher<'a, P = Self>;
type T: Teacher<P = Self>;
// more employees types here
}
pub trait Teacher {
type P: Employee;
fn to_borrowed_teacher(&self) -> <Self::P as Employee>::BT<'_>;
}
pub trait BorrowedTeacher<'a>: Copy + Clone {
type P: Employee;
fn to_employee_view(&self) -> EmployeeView<'a, Self::P>;
}
#[derive(Copy, Clone)]
pub enum EmployeeView<'a, P: Employee> {
BorrowedTeacher(P::BT<'a>),
}
pub enum OwnedEmployee<P: Employee> {
Teacher(P::T),
}
What I want to achieve is to have a function that is generic over the owned or borrowed enum version: I want to accept &OwnedEmployee
and BorrowedEmployee
as function arguments.
To this end, I tried to create a trait ToEmployeeView
that has a conversion function:
pub trait ToEmployeeView<P: Employee> {
fn to_employee_view(&self) -> EmployeeView<P>;
}
impl<'a, P: Employee> ToEmployeeView<P> for EmployeeView<'a, P> {
fn to_employee_view(&self) -> EmployeeView<P> {
*self
}
}
impl<P: Employee> ToEmployeeView<P> for OwnedEmployee<P> {
fn to_employee_view<'a>(&'a self) -> EmployeeView<'a, P> {
match self {
OwnedEmployee::Teacher(t) => t.to_borrowed_teacher().to_employee_view(),
}
}
}
However, this does not compile:
error: lifetime may not live long enough
--> src/lib.rs:34:9
|
32 | impl<'a, P: Employee> ToEmployeeView<P> for EmployeeView<'a, P> {
| -- lifetime `'a` defined here
33 | fn to_employee_view(&self) -> EmployeeView<P> {
| - let's call the lifetime of this reference `'1`
34 | *self
| ^^^^^ method was supposed to return data with lifetime `'a` but it is returning data with lifetime `'1`
|
= note: requirement occurs because of the type `EmployeeView<'_, P>`, which makes the generic argument `'_` invariant
= note: the enum `EmployeeView<'a, P>` is invariant over the parameter `'a`
= help: see <https://doc.rust-lang.org/nomicon/subtyping.html> for more information about variance
Here is a link to the playground.
I don't quite understand where the invariance comes from or how to fix the code. Here is a playground link to a modified version of the code that does not have a trait with an associated type with a lifetime parameter, and that code does compile.
Can someone help me out?
Traits, and generic associated types, are always invariant over their parameters. This is because they can be used with invariant types, so they have to be invariant.
Unfortunately, in Rust, there isn't way to declare "I want this lifetime to be covariant". So you cannot fix that easily.
There are two options to fix that. The simplest is instead of taking &EmployeeView
(coming from &self
) to take owned EmployeeView
. This can be done by taking self
instead of &self
, and implementing the trait for EmployeeView
and &OwnedEmployee
:
pub trait ToEmployeeView<'a, P: Employee> {
fn to_employee_view(self) -> EmployeeView<'a, P>;
}
impl<'a, P: Employee> ToEmployeeView<'a, P> for EmployeeView<'a, P> {
fn to_employee_view(self) -> EmployeeView<'a, P> {
self
}
}
impl<'a, P: Employee> ToEmployeeView<'a, P> for &'a OwnedEmployee<P> {
fn to_employee_view(self) -> EmployeeView<'a, P> {
match self {
OwnedEmployee::Teacher(t) => t.to_borrowed_teacher().to_employee_view(),
}
}
}
The more complex is to have a method, shrink_lifetime()
that does what covariance does automatically: converting the type to a type with shorter lifetime. The implementation will be as simpler as just returning self
as-is for implementing types (assuming they're covariant; invariant types cannot implement this method), but it cannot be default-provided and has to be implemented for each type.
pub trait BorrowedTeacher<'a>: Copy + Clone {
type P: Employee;
fn to_employee_view(&self) -> EmployeeView<'a, Self::P>;
fn shrink_lifetime<'b>(self) -> <Self::P as Employee>::BT<'b>
where
'a: 'b;
}
pub trait ToEmployeeView<P: Employee> {
fn to_employee_view(&self) -> EmployeeView<'_, P>;
}
impl<'a, P: Employee> ToEmployeeView<P> for EmployeeView<'a, P> {
fn to_employee_view(&self) -> EmployeeView<'_, P> {
let EmployeeView::BorrowedTeacher(bt) = *self;
EmployeeView::BorrowedTeacher(bt.shrink_lifetime())
}
}
impl<P: Employee> ToEmployeeView<P> for OwnedEmployee<P> {
fn to_employee_view<'a>(&'a self) -> EmployeeView<'a, P> {
match self {
OwnedEmployee::Teacher(t) => t.to_borrowed_teacher().to_employee_view(),
}
}
}