In my mind one of the ideal traits for a dependency injection container would look like:
pub trait ResolveOwn<T> {
fn resolve(&self) -> T;
}
I don't know how to implement this for certain T
. I keep stubbing my toes on variations and cousins of this error:
error[E0515]: cannot return value referencing local variable `X`
I'm used to dependency injection in C# where returning values referencing local variables is precisely how you implement the equivalent of that resolve
function.
Here's an illustration that focuses on this aspect of dependency injection:
struct ComplexThing<'a>(&'a i32);
struct Module();
impl Module {
fn resolve_foo(&self) -> i32 {
todo!()
}
pub fn resolve_complex_thing_1(&self) -> ComplexThing {
let foo = self.resolve_foo();
ComplexThing(&foo)
}
}
error[E0515]: cannot return value referencing local variable `foo`
--> src/lib.rs:12:9
|
12 | ComplexThing(&foo)
| ^^^^^^^^^^^^^----^
| | |
| | `foo` is borrowed here
| returns a value referencing data owned by the current function
See? There's that error.
My first instinct (again, coming from C#) is to give the local variable a place to live in the returned value, because the local variable is created here but it needs to live at least as long as the returned value. Hmm... that sounds sort of like returning a closure. Let's see how that goes...
pub fn resolve_complex_thing_2<'a>(&'a self) -> impl FnOnce() -> ComplexThing<'a> {
let foo = self.resolve_foo();
move || ComplexThing(&foo)
}
error[E0515]: cannot return value referencing local data `foo`
--> src/lib.rs:12:17
|
12 | move || ComplexThing(&foo)
| ^^^^^^^^^^^^^----^
| | |
| | `foo` is borrowed here
| returns a value referencing data owned by the current function
No joy. It doesn't work to package this closure up into a prettier type (like some impl of Into<ComplexThing<'a>>
) because it's fundamentally about returning a value referencing local data.
My next instinct is to somehow jam the local data into some kind of weak cache inside my Module
and then get a reference from there (undoubtedly unsafely). And then the weak cache will need to solve half of the hard problems in Computer Science (hint: the other hard problem is naming things). That's starting to sound an awful lot like... oh no. Garbage collection!
I also thought about inverting the flow of control. It's hideous and still doesn't work:
impl Module {
pub fn use_foo<T>(&self, f: impl FnOnce(i32) -> T) -> T {
(f)(42)
}
pub fn use_complex_thing<'a, T>(&'a self, f: impl FnOnce(ComplexThing<'a>) -> T) -> T {
self.use_foo(
|foo| (f)(ComplexThing(&foo)),
)
}
}
error[E0597]: `foo` does not live long enough
--> src/lib.rs:12:36
|
10 | pub fn use_complex_thing<'a, T>(&'a self, f: impl FnOnce(ComplexThing<'a>) -> T) -> T {
| -- lifetime `'a` defined here
11 | self.use_foo(
12 | |foo| (f)(ComplexThing(&foo)),
| -----------------^^^^--
| | | |
| | | `foo` dropped here while still borrowed
| | borrowed value does not live long enough
| argument requires that `foo` is borrowed for `'a`
My last instinct is to hack around the restriction against moving a value with active borrows, because then I could trick the compiler. My attempts at implementing that resulted in a type that's impossible to use correctly — it ended up requiring knowledge that only the compiler has and seemed to introduce undefined behavior at every turn. I won't bother reproducing that code here.
It seems like it's impossible to return any owned instances of types containing (non-singleton) references.
Assuming that's true, that means there are entire classes of types that simply cannot be created with a dependency injection container in Rust.
Surely I'm missing something?