I'm trying to implement an abstraction for database operations for my web backend. My goal is to be able to swap out the implementation with mocks so I can easily write unit tests for my business logic. I've come up with the following:
struct Connection;
/// A session essentially wraps an open database connection
trait Session<'a> {
fn new(conn: &'a mut Connection) -> Self;
}
/// A specific trait for a set of database operations needed
/// in some business logic
trait MySession<'a> {
fn create_entity(&'a mut self);
fn read_entity(&'a mut self);
}
struct MySessionImpl<'a> {
conn: &'a mut Connection,
}
impl<'a> Session<'a> for MySessionImpl<'a> {
fn new(conn: &'a mut Connection) -> Self {
Self { conn }
}
}
impl<'a> MySession<'a> for MySessionImpl<'a> {
fn create_entity(&'a mut self) { todo!() }
fn read_entity(&'a mut self) { todo!() }
}
/// A trait that I can pass a closure to work with a session.
/// It's responsible for opening and closing a connection.
trait SessionFactory {
fn run<'a, TFunc, TSession>(&self, f: TFunc)
where
TFunc: FnOnce(TSession),
TSession: Session<'a> + 'static;
}
struct DefaultSessionFactory;
impl SessionFactory for DefaultSessionFactory
{
fn run<'a, TFunc, TSession>(&self, f: TFunc)
where
TFunc: FnOnce(TSession),
TSession: Session<'a> + 'static,
{
let mut conn = Connection;
let session = TSession::new(&mut conn);
f(session);
}
}
pub fn main() {
let session = DefaultSessionFactory;
// In domain code
let _result = session.run(|mut s: MySessionImpl| {
s.read_entity();
// do some stuff
s.create_entity()
});
}
Unfortunately, the compiler is emitting the following errors:
Compiling playground v0.0.1 (/playground)
error[E0597]: `conn` does not live long enough
--> src/main.rs:45:37
|
39 | fn run<'a, TFunc, TSession>(&self, f: TFunc)
| -- lifetime `'a` defined here
...
44 | let mut conn = Connection;
| -------- binding `conn` declared here
45 | let session = TSession::new(&mut conn);
| --------------^^^^^^^^^-
| | |
| | borrowed value does not live long enough
| argument requires that `conn` is borrowed for `'a`
46 | f(session);
47 | }
| - `conn` dropped here while still borrowed
error[E0597]: `s` does not live long enough
--> src/main.rs:55:9
|
54 | let _result = session.run(|mut s: MySessionImpl| {
| -----
| |
| binding `s` declared here
| has type `MySessionImpl<'1>`
55 | s.read_entity();
| ^--------------
| |
| borrowed value does not live long enough
| argument requires that `s` is borrowed for `'1`
...
58 | });
| - `s` dropped here while still borrowed
I'm not sure how to correctly specify the lifetimes here and would appreciate any help
An explicit lifetime on &(mut) self is almost never what you need.
Your trait MySession declares a lifetime 'a associated with the borrow of self. You then declare in trait SessionFactory that method run() has a user-chosen lifetime that must correspond to the lifetime parameter of the MySession trait, and therefore to the borrow of self in its methods.
However, the lifetime may be chosen by the user to be arbitrarily long (even eg. 'static) β so this doesn't make any sense!
There's another problematic requirement: Session<'a> + 'static can never be true in practice, if the type implementing Session<'_> actually uses the lifetime parameter. That means it can contain a borrow with an arbitrary(ly short) lifetime, which precludes it from being 'static.
Your Session trait is actually a roundabout factory pattern, of which the correct "be generic over an arbitrary lifetime" constraint is not straightforward to describe in code.
Essentially, you want to get a type that depends on a lifetime that you choose inside the function body. This means that
ordinary generics are out of question (as the caller doesn't control the lifetime), and the naΓ―ve approach using a HRTB also fails, as a single type SessionImpl<'a> can't satisfy Session<'b>for every 'b.
The final solution involves being higher-kinded over the lifetime, which then leaks into the interface in the form of a generic associated type and explicit HRTBs in the function signature, but it is doable.
When you put 'a on the trait, it refers to some long-lived loan of something external, that isn't stored in this object, and that has been created earlier before self has been created, and will stay unchanged for the entire duration of the usage of the object implementing this trait.
When you put the same 'a on &mut you're saying the duration of the loan is the same for both self and the other loan, and the mut part says it's required to be absolutely exclusive loan, and must never be shared with anything else and cannot or any shorter than the maximum duration of 'a.
These two together create totally useless nonsense that says all of self gets borrowed once and forever and is not allowed to be used ever again, because the exclusive impossible to shorten loan of self is tied to the external longer-than-self exists lifetime applied to the trait as a whole.
@paramagnetic Thanks for your explanation and your solution! I would be curious, do you think the way I approached my problem to create an abstraction for my database operations is generally wrong/complicated/unergonomic? Is the factory pattern a bad practice in Rust? Are there established patterns to create the kind of abstraction that I want (my research was rather unsuccessful)?
Thank you for your explanation on what's the root of the problem, @kornel. You and @paramagnetic seem to have a deep understanding of lifetimes, are there any resources apart from for example The Book or the Nomicon that you could recommend me. I would love to gain more knowledge about lifetimes, myself
For me, it's a simple pattern match. If I see something that I want to work with borrowed data my function implementation specifies, I know I'll need a HRTB.
You don't learn lifetimes by reading some books. You learn lifetimes by writing code and fixing errors.
I don't know. I've learned them the hard way, before Rust had good learning resources
But if you're a beginner, you can get productive in Rust with one rule:
Don't put references in structs. If you think you need them, you may be mistaking them for pointer types or references from other languages, which are much more general-purpose than Rust's temporary loans that turn structs into temporary views bound to a scope. Rust's references are by design incapable of storing data "by reference". Rust's standard library already provides temporary view types like lock guards, by-ref iterators, and Cow, so you rarely need to roll your own. In real Rust code bases it's possible to write 10K lines code without a single <'a> anywhere.
Use temporary references only for function arguments. In fn new() consider self-contained/owned types too. Remember that Box and Arc are by-reference types too, and Vec/String also store data via indirection, and never copy their data implicitly.