Regarding the initial problem, this is something I've been thinking a lot as it enables compile-time hierarchical dependency injection. You can have a base layer defining your dependencies, and then a child layer for local partial overrides. The compiler automatically picks values from the top of the current stack.
A particularly important use case for me are RPC clients. You may want to configure your client, for example with a base URL, auth token, timeout, etc. Code using it can then create queries where you are able to override these settings. I want two things at the handler level:
- Ensure at compile time that the base URL is defined at least in one place in the stack
- Resolve at compile what is the top-most value for the base URL
Here is simplified implementation from a project I've been working on: Rust Playground
The try_merge_triple_cx*
test cases in particular should show you how it looks from a user point of view. There are also some commented tests showing rejection when the base URL is not set. The values to use are resolved at compile time.
Further below there is a more concrete example showing how it can be used for my use case involving compile-time DI for an RPC client with override support.
The original code is more complex and I don't have too much time to polish the Playground example, but it should show the core ideas with compile time TyNone
, TySome
, TryGetBaseUrl
, GetBaseUrl
and TupGetBaseUrl
types/traits. This can be adapted to work on references or have a more generic getter trait. My real code uses for<'a> (&'a QueryCx, &'a ClientCx): Get<BaseUrl> + Get<AuthToken>
as the trait bound for handlers for example. (You can get to a point where there's basically no clone or dynamic lookup.)
I should probably take some time to write a more in-depth description of this approach, but I hope that the playground link will help you.
I'm pasting the most important part as part of the post, this how the hierarchy is handled, using a case distinction on associated types. Since Rust does not let me check for !Trait
, the workaround is to match on an associated type where one represents impl Foo
and the other impl !Foo
. TryGetBaseUrl<Output=TyNone>
is the negative version, and TryGetBaseUrl<Output=Url>
is the positive version.
trait TupGetBaseUrl<Cx: TryGetBaseUrl, Out = <Cx as TryGetBaseUrl>::Output> {
type TupOut;
fn tup_get_base_url(&self) -> Self::TupOut;
}
impl<Cx0, Cx1> TupGetBaseUrl<Cx0, TyNone> for (Cx0, Cx1)
where
Cx0: TryGetBaseUrl<Output=TyNone>,
Cx1: TryGetBaseUrl,
{
type TupOut = Cx1::Output;
fn tup_get_base_url(&self) -> Self::TupOut {
self.1.try_get_base_url()
}
}
impl<Cx0, Cx1> TupGetBaseUrl<Cx0, Url> for (Cx0, Cx1)
where
Cx0: TryGetBaseUrl<Output=Url>,
{
type TupOut = Cx0::Output;
fn tup_get_base_url(&self) -> Self::TupOut {
self.0.try_get_base_url()
}
}
impl<Cx0, Cx1> TryGetBaseUrl for (Cx0, Cx1)
where
Cx0: TryGetBaseUrl,
Self: TupGetBaseUrl<Cx0>,
{
type Output = <Self as TupGetBaseUrl<Cx0>>::TupOut;
fn try_get_base_url(&self) -> Self::Output {
self.tup_get_base_url()
}
}
You can generalize this idea by using Output=TySome<T>
for the positive case. From what i remember, the context trait and values that can be extract are still somewhat coupled. Full support for negative traits would simplify the code.