Search code examples
rustlifetime

Lifetime variance issue when using trait with associated type that has a lifetime


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?


Solution

  • 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(),
            }
        }
    }