How to write an existential lifetime for an associated type?

Hi, all,

I've got a problem where I can't correctly write the RHS of an associated type parameter. Naively this looks like an application for GATs, but there are a couple of problems in doing so. First, ordinarily callers of the interface below would have no need to know that the implementation of the DbStorage type requires a lifetime bound; this should be opaque to the callers of with_storage. Second, even if I do write the top-level associated type as DbStorage<'a> and then write with_storage as fn with_storage<for <'a> F: FnOnce(&mut DbWrapper<Self::DbStorage<'a>>) -> ()>(&mut self, callback: F) -> (); I find that callers then see errors stating that 'a does not live long enough.

I've shrunk this down to try to represent the minimal accurate example I can come up with; please let me know if there's additional information that I can provide to help clarify the question.

use std::collections::BTreeMap;

// this is our low-level data storage capability
trait StorageCap {
    type IdType;
    
    fn get_value(&self, id: Self::IdType) -> String;
}

// This is a high-level wrapper object around our low-level capability.
// It delegates some part of all its operations to the lower level cap,
// it might have a cache attached to it to save calling the lower level
// cap sometimes, etc.
struct DbWrapper<S: StorageCap>(S);

impl<T: Eq, S: StorageCap<IdType = T>> DbWrapper<S> {
   fn get_value_filtered(&self, id: T) -> String {
       let res = self.0.get_value(id);
       if res == "hello" {
           "don't say hello!".to_string()
       } else {
           res
       }
   }   
}

// we can store data in a BTreeMap
impl StorageCap for BTreeMap<u32, String> {
    type IdType = u32;
    
    fn get_value(&self, id: u32) -> String {
        self.get(&id).unwrap().clone()
    }
}

struct Transaction<'a> {
    _stuff: &'a (),
}

// we can also potentially store data _using_ a transactional database
impl<'a> StorageCap for Transaction<'a> {
    type IdType = u32;
    
    fn get_value(&self, id: u32) -> String {
        "this would actually do something".to_string()
    }
} 

// This is the high-level user-facing trait that we want to implement. The
// caller doesn't actually need to know the concrete type of `DbStorage` 
// or care about its lifetime; the lifetime of the value they're given in
// the callback will be bound to the lifetime of `Self` or some shorter
// lifetime.
trait DbCap { 
    type DbStorage: StorageCap<IdType = u32>;
    
    fn with_storage<F: FnOnce(&mut DbWrapper<Self::DbStorage>) -> ()>(&mut self, callback: F) -> ();
}

struct MemoryDb {
    storage: DbWrapper<BTreeMap<u32, String>>,
}

// Writing a callback that accesses the concrete, in-memory storage is easy
impl DbCap for MemoryDb {
    type DbStorage = BTreeMap<u32, String>;
    
    fn with_storage<F: FnOnce(&mut DbWrapper<Self::DbStorage>) -> ()>(&mut self, callback: F) -> () {
         callback(&mut self.storage);
    }
}

struct TransactionDb {
    stuff: (),
}

impl TransactionDb {
    fn transaction<'a>(&'a mut self) -> Transaction<'a> {
        Transaction { _stuff: &self.stuff }
    }
}

// Writing a callback that uses the transactional database seems impossible,
// because you can't properly name the lifetime.
impl DbCap for TransactionDb {
    type DbStorage = Transaction<'???>;
    
    fn with_storage<F: FnOnce(&mut DbWrapper<Self::DbStorage>) -> ()>(&mut self, callback: F) -> () {
        let tx = self.transaction();
        let mut wrapper = DbWrapper(tx);
        callback(&mut wrapper)
    }
}

(Playground)

When you use explicit lifetimes, they tend to spread transitively throughout your code. I managed to get your code to compiling just by adding lifetime parameters to all the structs.

I'm not sure what bound you're talking about.

FYI FnOnce(&mut DbWrapper<Self::DbStorage<'_>>) means the same thing.

Can you demonstrate such errors? This worked for me in the playground.

trait DbCap { 
    // You could remove `Self: 'a` (but that may be a barrier for some implementors)
    type DbStorage<'a>: StorageCap<IdType = u32> where Self: 'a;
    fn with_storage<F>(&mut self, callback: F) -> ()
    where
        F: FnOnce(&mut DbWrapper<Self::DbStorage<'_>>) -> ();
}

impl DbCap for TransactionDb {
    type DbStorage<'a> = Transaction<'a>;
    
    fn with_storage<F: FnOnce(&mut DbWrapper<Self::DbStorage<'_>>) -> ()>(&mut self, callback: F) -> () {
        let tx = self.transaction();
        let mut wrapper = DbWrapper(tx);
        callback(&mut wrapper)
    }
}

This is very interesting, thanks for taking the time!

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.