Help needed - lifetime'ed associated types in trait

Setup -

  • Service trait that has multiple implementations
  • Context type that adds a context to any Service
  • ContextedService trait that provides a with_context() method that returns a Guard type that captures the context within it. Guard object implements the Service trait - ie, Guard acts as the Service itself, but scoped within the context.
  • Need to independently create both Service and Context objects and associate each other using ContextedService trait.
  • Have a design for it, but it fails due to lifetime issues / trait boundary definitions when service and context objects are created across function boundaries.

Below is what i came up with, but it has the issue mentioned above.

// --- Service --- //
trait Service {
    async fn get(&self, prefix: String) -> String;
}

// MyService is an implementation for Service trait. There can be many impls for ServiceTrait
struct MyService {
    name: String,
}

impl Service for MyService {
    async fn get(&self, prefix: String) -> String {
        format!("{prefix} works")
    }
}

// --- Context && ContextGuard --- //
// Context captures a scope within application logic
struct Context {
    scope: String,
}

// ContextGuard captures references to Service and Context
struct ContextGuard<'svc, 'ctx, S>
where
    S: Service,
{
    service: &'svc S,
    context: &'ctx Context,
}

// Overall idea of ContextedService is to prefix-inline a context before sending it to underlying service
impl<'svc, 'ctx, S> Service for ContextGuard<'svc, 'ctx, S>
where
    S: Service,
{
    async fn get(&self, prefix: String) -> String {
        let contexted_prefix = format!("{}::{}", self.context.scope, prefix);
        self.service.get(contexted_prefix).await
    }
}


// --- ContextedService --- //
// ContextedService implements Service trait and the implementation is bounded within the lifetimes of both Service and Context params
trait ContextedService {
    // Guard captures the underlying service and "scopes" (or decorates) it within context's lifetime
    type Guard<'svc, 'ctx, S>
    where
        S: 'svc + Service,
        Self: 'svc + Service;

    // Creates the guard object capturing Service (Self) and Context by reference
    fn with_context<'svc, 'ctx>(
        &'svc self,
        context: &'ctx Context,
    ) -> Self::Guard<'svc, 'ctx, Self>
    where
        Self: Service + Sized;
}

impl ContextedService for MyService {
    type Guard<'svc, 'ctx, S> = ContextGuard<'svc, 'ctx, S>
    where
        S: 'svc + Service,
        Self: 'svc + Service;

    fn with_context<'svc, 'ctx>(
        &'svc self,
        context: &'ctx Context,
    ) -> ContextGuard<'svc, 'ctx, MyService>
    where
        Self: Service,
    {
        ContextGuard {
            service: self,
            context,
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let s = MyService { name: "testing_service".to_string() };
    println!("{}", s.get("test".to_string()).await);

    let context = Context { scope: "scoped".to_string() };
    let cs = s.with_context(&context);
    println!("{}", cs.get("test".to_string()).await);

    call_contexted_from_inside(&s).await;
    Ok(())
}


// This fn fails to compile
async fn call_contexted_from_inside<'svc, 'ctx, S>(service: &'svc S)
where
    S: Service + ContextedService,
    <S as ContextedService>::Guard<'svc, 'ctx, S>: Service,
{
    let context = Context { scope: "scoped_in_fn".to_string() };
    let cs = service.with_context(&context);
    println!("{}", cs.get("test_in_fn".to_string()).await);
}

Rust playground link

Overall, what i am looking for is to have a type (eg: ContextedService) that captures references to both an actual Service and a Context, and use the type as if it is a Service, but scoped within a context. What would be the best way to achieve this?

The problem with your call_contexted_from_inside method is that it tries to use a lifetime parameter 'ctx to refer to a lifetime of a borrow that's only created inside of the function (&context), but lifetime parameters are always referring to borrows outside of the function.

The usual way direct way around this is to use HRTBs, such as e.g.

- async fn call_contexted_from_inside<'svc, 'ctx, S>(service: &'svc S)
+ async fn call_contexted_from_inside<'svc, S>(service: &'svc S)
where
    S: Service + ContextedService,
-   <S as ContextedService>::Guard<'svc, 'ctx, S>: Service,
+   for<'ctx> <S as ContextedService>::Guard<'svc, 'ctx, S>: Service,

however, this runs into some … unfortunate … error message including a note

note: this is a known limitation of the trait solver that will be lifted in the future

However, the good news is: as far as I understand from your description, the associated type Guard is supposed to always implement Service anyways, and moving this bound to the trait declaration solves the problem indeed:

trait ContextedService {
-   type Guard<'svc, 'ctx, S>
+   type Guard<'svc, 'ctx, S>: Service
    where
        S: 'svc + Service,
        Self: 'svc + Service;

    …
}


// This fn fails to compile
async fn call_contexted_from_inside<'svc, S>(service: &'svc S)
where
    S: Service + ContextedService,
-   for<'ctx> <S as ContextedService>::Guard<'svc, 'ctx, S>: Service,
{
    let context = Context { scope: "scoped_in_fn".to_string() };
    let cs = service.with_context(&context);
    println!("{}", cs.get("test_in_fn".to_string()).await);
}
2 Likes

Thanks a lot @steffahn! That worked!

(How did you add the line diffs as captured above? Wasnt able to figure it out!)

After making the suggested changes, also did some additional cleanup as below to not have 3rd generic parameter in Guard as that parameter will be the impled service type itself.

@@ -45,32 +45,26 @@ where
// ContextedService implements Service trait and the implementation is bounded within the lifetimes of both Service and Context params
trait ContextedService {
    // Guard captures the underlying service and "scopes" (or decorates) it within context's lifetime
-    type Guard<'svc, 'ctx, S>
+    type Guard<'svc, 'ctx> : Service
    where
-        S: 'svc + Service,
-        Self: 'svc + Service;
+        Self: 'svc;

    // Creates the guard object capturing Service (Self) and Context by reference
    fn with_context<'svc, 'ctx>(
        &'svc self,
        context: &'ctx Context,
-    ) -> Self::Guard<'svc, 'ctx, Self>
-    where
-        Self: Service + Sized;
+    ) -> Self::Guard<'svc, 'ctx>;
}

impl ContextedService for MyService {
-    type Guard<'svc, 'ctx, S> = ContextGuard<'svc, 'ctx, S>
+    type Guard<'svc, 'ctx> = ContextGuard<'svc, 'ctx, MyService>
    where
-        S: 'svc + Service,
-        Self: 'svc + Service;
+        Self: 'svc;

    fn with_context<'svc, 'ctx>(
        &'svc self,
        context: &'ctx Context,
    ) -> ContextGuard<'svc, 'ctx, MyService>
-    where
-        Self: Service,
    {
        ContextGuard {
            service: self,
```diff
- foo
+ bar
```
- foo
+ bar
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.