Trait with method returning Self with updated lifetime

I'm trying to define a trait with a method that consumes an implementation (say: Impl<'this>) of the trait and is supposed to produce a new instance of the same implementation (say: Impl<'next>) with an updated lifetime.

trait TraitWithLifetime<'this> {
    fn switch_to_next<'next>(
        self,
        next: Something<'next>,
    // How can I return Self with an updated lifetime?
    // - Self<'next> isn't allowed.
    // - impl TraitWithLifetime<'next> is not strong enough as
    //   I need to save the result in a struct, ie. I need a specific
    //   return type.
    // - Self + 'next doesn't work because it will return Impl<'this>
    //   rather than Impl<'next>.
    // - Let Impl return Impl<'next> while the trait defines
    //   impl TraitWithLifetime<'next> (refined return type) doesn't
    //   work because the caller would then have to be aware of
    //   the specific implementation.
    ) -> ???;
}

The trait is intended to be used like this:

struct Impl<'a> {...}
impl <'a> TraitWithLifetime<'a> for Impl<'a> {...}

fn switch<'this, 'next> (
    impl_this: Impl<'this>,
    next: Something<'next>,
) -> Impl<'next> {
    impl_this.switch_to_next(next)
}

Any ideas?

There's no syntax for customizing lifetime parameters like that. Self always means the exact type with all lifetimes the same.

I think you may to have to create an associated type on the trait for this, and return Self::TheAssociatedType<'next>.

Or you can use the limited -> impl Trait support to return impl TraitWithLifetime<'next>.

1 Like

Hi @kornel ,

thanks for taking the time to look into this!

I had tried this before and unfortunately it doesn't seem to work. Introducing an associated type with a lifetime parameter requires crazy restrictions on that lifetime parameter that contradict the requirement.

The error produced by the compiler is:

error: missing required bounds on `NextState`
  --> mod.rs:14:5
   |
14 |     type NextState<'new_rx, 'new_tx>: TraitWithLifetime<'new_rx, 'new_tx>;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-
   |                                                              |
   |                                                              help: add the required where clauses: `where Self: 'rx, Self: 'tx, 'tx: 'new_tx, Self: 'new_rx, 'new_rx: 'rx, Self: 'new_tx, 'new_tx: 'tx`
   |
   = note: these bounds are currently required to ensure that impls have maximum flexibility
   = note: we are soliciting feedback, see issue #87479 <https://github.com/rust-lang/rust/issues/87479> for more information

I simplified my example a bit, that's why you see two lifetime parameters in the error message.

UPDATE: And even if if that worked then you get back type TraitWithLifetime::NextState which the caller knows basically nothing about, especially it won't enforce the same implementation with different generics only.

This doesn't work, because I need a specific return type to be saved in a struct.

Seems like there is no solution then?

There may be a solution depending on what you're trying to do.

Rust doesn't have generic type constructor paramters, where you could do something like:

// Imaginary language feature
fn switch<'from, 'into, T<'_>>(from: T<'from>) -> T<'into> { todo!() }

But you can emulate them do various degrees with associated types.

// This version requires the type constructor to have at most one lifetime
// parameter
type Constructor {
    Ty<'a>;
}

impl Constructor for &str {
    Ty<'a> = &'a str;
}

impl Constructor for () {
    Ty<'a> = ();
}

For example.

Things get trickier, sometimes significantly so, if you need to support type constructors with multiple lifetimes, or otherwise effectively need conditional higher-ranked bounds in any way.

// Imaginary conditional higher-ranked bound language feature
where T: for<'a where 'b: 'a> ...
1 Like

Hi @quinedot,

you must have spent considerable time to answer my question, thank you! I especially like the detailed example, you provided. Very helpful, much more than I thought would be possible, probably similar to what @kornel actually had in mind without me recognizing the potential. Looks promising.

Alternatively I might be able to organize information differently, keeping long lived structs separate from short lived references and keeping the latter in async functions rather than in the base object. This might allow me to cross await points without having to "switch" lifetimes. We'll see.

In any case your responses helped me to unblock my thinking. :slight_smile: