Enforce that an associated type does not contain an explicit lifetime

Is there any way to enforce that a associated type does not contain an explicit lifetime i.e. in terms of a reference? In the example below, the compiler (correctly) rejects compiling with E0597:

/// A result which may outlive its creator.
trait Result {}
impl Result for () {}

trait FactoryTrait {
    // How do I annotate that the type must not contain a borrow of Self?
    type Result: Result;
    fn get(self) -> Self::Result;
}

struct Factory<'a, T>(&'a T)
where
    Self: FactoryTrait;

trait FactoryCreator: Default
where
    for<'a> Factory<'a, Self>: FactoryTrait,
{
    fn create<'a>(&'a self) -> Factory<'a, Self>;
}

#[derive(Default)]
struct StructA {}

impl FactoryCreator for StructA {
    fn create<'a>(&'a self) -> Factory<'a, Self> {
        Factory(self)
    }
}

impl<'a> FactoryTrait for Factory<'a, StructA> {
    type Result = ();
    fn get(self) -> Self::Result {
        ()
    }
}

fn spawn<T: 'static + FactoryCreator>() -> impl Result
where
    for<'a> Factory<'a, T>: FactoryTrait,
{
    let factory_creator = T::default();
    let factory = factory_creator.create();
    factory.get()
}

However, as long FactoryTrait::Result does not depend on Factory<'_, StructA> that should be a safe operation. Is there any way to enforce this and satisfy the compiler?

So the main problem here is that the type for the impl Result return value can’t be written.

If you’d try to remove the impl Result and write down the concrete type itself, i.e. something like

fn spawn<T: 'static + FactoryCreator>() -> <Factory<'lifetime, T> as FactoryTrait>::Result
where
    for<'a> Factory<'a, T>: FactoryTrait,
{ …

it’s impossible to write the right lifetime for 'lifetime. The lifetime in question is a lifetime of a local borrow, there’s nothing you can change about this with any kind of bound in the FactoryTrait trait itself.

One approach could be to somehow redesign the whole thing in a way that Factory<'a, T>: FactoryTrait isn’t a thing anymore but instead there’s some kind of bound on T itself. Maybe you don’t need the FactoryTrait after all? It’s a bit hard to give good suggestions with such a hypothetical code example.

Another approach, probably the one inflicting minimal API change, is to just duplicate the Result type into FactoryCreator and change the for<'a> Factory<'a, Self>: FactoryTrait bound to disallow Result of the generated factories to depend on the factory type itself, instead it would be fixed depending on the FactoryCreator type alone.

/// A result which may outlive its creator.
trait Result {}
impl Result for () {}

trait FactoryTrait {
    // How do I annotate that the type must not contain a borrow of Self?
    type Result: Result;
    fn get(self) -> Self::Result;
}

struct Factory<'a, T>(&'a T)
where
    Self: FactoryTrait;

trait FactoryCreator: Default
where
-   for<'a> Factory<'a, Self>: FactoryTrait,
+   for<'a> Factory<'a, Self>: FactoryTrait<Result = Self::FactoryResult>,
{
+   type FactoryResult: Result;
    fn create<'a>(&'a self) -> Factory<'a, Self>;
}

#[derive(Default)]
struct StructA {}

impl FactoryCreator for StructA {
+   type FactoryResult = ();
    fn create<'a>(&'a self) -> Factory<'a, Self> {
        Factory(self)
    }
}

impl<'a> FactoryTrait for Factory<'a, StructA> {
    type Result = ();
    fn get(self) -> Self::Result {
        ()
    }
}

fn spawn<T: 'static + FactoryCreator>() -> impl Result
where
-   for<'a> Factory<'a, T>: FactoryTrait,
+   for<'a> Factory<'a, T>: FactoryTrait<Result = T::FactoryResult>,
{
    let factory_creator = T::default();
    let factory = factory_creator.create();
    factory.get()
}
3 Likes

Another option is to "lift" the output type from being specified by spawn to specified by its caller:

fn spawn<T: FactoryCreator, R>() -> R
where
    for<'a> Factory<'a, T>: FactoryTrait<Result = R>,
2 Likes

Wow @steffahn, thank you for the extremly well-written and detailed answer! Solution 2 is the solution I searched for - and quite smart!
Once generic associated types are finally there, this nightmare of boilerplate is hopefully not anly longer required...

With a little effort you can “encode” GATs (only generic over lifetimes) in ordinary stable rust traits. E.g. starting with something like this:

#![feature(generic_associated_types)]
#![allow(unused)]

/// A result which may outlive its creator.
trait ResultTrait {}
impl ResultTrait for () {}

trait FactoryTrait {
    type Result: ResultTrait;
    fn get(self) -> Self::Result;
}

trait FactoryCreator: Default {
    type Factory<'a>: FactoryTrait<Result = Self::Result>;
    type Result: ResultTrait;
    fn create(&self) -> Self::Factory<'_>;
}

#[derive(Default)]
struct StructA {}

impl FactoryCreator for StructA {
    type Factory<'a> = FactoryA<'a>;
    type Result = ();
    fn create(&self) -> FactoryA<'_> {
        FactoryA(self)
    }
}

struct FactoryA<'a>(&'a StructA);

impl<'a> FactoryTrait for FactoryA<'a> {
    type Result = ();
    fn get(self) -> Self::Result {
        ()
    }
}

fn spawn<T: FactoryCreator>() -> T::Result {
    let factory_creator = T::default();
    let factory = factory_creator.create();
    factory.get()
}

you can turn it into

#![allow(unused)]

/// A result which may outlive its creator.
trait ResultTrait {}
impl ResultTrait for () {}

trait FactoryTrait {
    type Result: ResultTrait;
    fn get(self) -> Self::Result;
}

trait FactoryCreatorHasFactory<'a, Result> {
    type Factory: FactoryTrait<Result = Result>;
}
type FactoryCreatorFactory<'a, Self_> = <Self_ as FactoryCreatorHasFactory<'a, <Self_ as FactoryCreator>::Result>>::Factory;

trait FactoryCreator: Default + for<'a> FactoryCreatorHasFactory<'a, Self::Result> {
    type Result: ResultTrait;
    fn create(&self) -> FactoryCreatorFactory<'_, Self>;
}

#[derive(Default)]
struct StructA {}

impl<'a> FactoryCreatorHasFactory<'a, ()> for StructA {
    type Factory = FactoryA<'a>;
}
impl FactoryCreator for StructA {
    type Result = ();
    fn create(&self) -> FactoryA<'_> {
        FactoryA(self)
    }
}

struct FactoryA<'a>(&'a StructA);

impl<'a> FactoryTrait for FactoryA<'a> {
    type Result = ();
    fn get(self) -> Self::Result {
        ()
    }
}

fn spawn<T: FactoryCreator>() -> T::Result {
    let factory_creator = T::default();
    let factory = factory_creator.create();
    factory.get()
}

You might also like to add a bound

    type Factory<'a>: FactoryTrait<Result = Self::Result>
    where
        Self: 'a;

to the GAT to allow for e.g. a reference &self to be stored in the result of self.create() for a type that isn’t Self: 'static. Such a bound can be represented in stable Rust e.g. like this:

#![allow(unused)]

/// A result which may outlive its creator.
trait ResultTrait {}
impl ResultTrait for () {}

trait FactoryTrait {
    type Result: ResultTrait;
    fn get(self) -> Self::Result;
}

// only ever used with _Constraint == &'a Self, gives
// rise to an implicit Self: 'a constraint which
// restricts the `for<'a> FactoryCreatorHasFactory<'a, Self::Result>`
// supertrait of `FactoryCreator` to only range about `'a` that fulfill `Self: 'a`.
trait FactoryCreatorHasFactory<'a, Result, _Constraint = &'a Self> {
    type Factory: FactoryTrait<Result = Result>;
}
type FactoryCreatorFactory<'a, Self_> = <Self_ as FactoryCreatorHasFactory<'a, <Self_ as FactoryCreator>::Result>>::Factory;

trait FactoryCreator: Default + for<'a> FactoryCreatorHasFactory<'a, Self::Result> {
    type Result: ResultTrait;
    fn create(&self) -> FactoryCreatorFactory<'_, Self>;
}

#[derive(Default)]
struct StructA {}

impl<'a> FactoryCreatorHasFactory<'a, ()> for StructA {
    type Factory = FactoryA<'a>;
}
impl FactoryCreator for StructA {
    type Result = ();
    fn create(&self) -> FactoryA<'_> {
        FactoryA(self)
    }
}

struct FactoryA<'a>(&'a StructA);

impl<'a> FactoryTrait for FactoryA<'a> {
    type Result = ();
    fn get(self) -> Self::Result {
        ()
    }
}

fn spawn<T: FactoryCreator>() -> T::Result {
    let factory_creator = T::default();
    let factory = factory_creator.create();
    factory.get()
}
2 Likes

IMHO, this example it so informative that it deserves an inclusion within in the documentation. I will certainly use this pattern of "splitting a trait into one for the methods and one for the lifetime" more often. Thanks a lot!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.