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)
}
}