Suggestion: Dealing with ServiceProvider instance type with .boxed()
Hey! I've been experimenting with teloc for a while now as I'm surveing crates enabling Hexagonal Architecture in Rust.
In the context of leveraging IoC pattern that teloc implements, I faced the inefficiency of having to modify over and over the type alias for ServiceProvider instance.
This is an issue affecting every crate exploiting Rust generics support, but I came across an interesting pattern that the axum fellows implemented to deal with it.
In axum, HTTP routes are implemented composing generic type, leading to big types that only the compiler will eventually know. To make building routes developer-friendly, they provide a .boxed() method returning a BoxRoute that erases the big generic type.
References are available here:
Do you think this might be implementable in teloc as well? It would make declaring containers much more ergonomic, imho.
Anyhow, keep up the good work! 💪
Thanks for the issue!
I know this way with using type aliases is inefficient, but there are two tradeoffs:
- Type-level computations, so rustc won't compile when you request for a non-existent dependency. In this case we need to store all the types we have in the
ServiceProviderinstance, because otherwise rustc cannot satisfy which types are stored in theServiceProvider. - Runtime resolves. In this case we can write something like
BoxedServiceProvider, but rustc cannot help us at compile time.
As you can see, teloc uses first approach, so if it is changed to the second, it will be a completely different library. The only way to get out of both tradeoffs is with global type inference. With global type inference, it will be possible to write ServiceProvider type alias like type Provider = ServiceProvider<_>;, but it will never be in the Rust.
In axum case boxing is possible due to Router need only one method call, while ServiceProvider need many methods: resolve::<Type1>(), resolve::<Type2>(), etc.
But, we can combine both approaches. But, user must writing something by yourself yet:
trait MyProvider: Resolver<Dep1> + Resolver<Dep2> + /* list all dependencies you need in the app */ { }
impl<P> MyProvider for P where P: Resolver<Dep1> + Resolver<Dep2> + /* ... */ { }
type MyProvider = Box<dyn MyProvider>;
Then you can call all .resolve() method for the types you listed in the trait MyProvider. Pros: human readability, compiler checks. Cons: you have to change MyProvider signature every time when you add/remove dependency that used from the top-level.
There are possible simplification by adding a macro:
define_resolver![MyProvider => Dep1, Dep2, Dep3];
fn create() -> MyProvider { ... }
fn use(provider: MyProvider) { ... }
I think this is the most efficient way, and I can't think of something better. What do you think about it, @leonardoarcari?
Now that's super interesting! Thanks for the great explanation and the extremely reasonable workaround. Such a solution is very close to another crate I was evaluating (shaku), which unfortunately does not support dyn dependencies implemented by the same concrete struct.
This is my first language with such a strong macro system and I'm always amazed by how powerful it can be!
Thanks! 🙂