There are a few advantages to the effect-based solution which my current proposal lacks. Here is my attempt at fixing that!
Capabilities
The effects-solution proposes using capabilities to "namespace" the components of interest rather than types. This gives two special superpowers:
- The ability to provide multiple instances of the same type but with different logical uses. (e.g.
MyCoolType
being both a logger
and a tracer
)
- The ability to accept an abstract component implementing a trait rather than a specific component (e.g. taking any
logger: Logger
rather than just MyLogger
).
We can easily extend the current proposal to support these superpowers by defining special NamedRef<'a, N, T>
and NamedMut<'a, N, T>
structures. These are just newtypes around immutable and mutable references respectively and function in almost the same way as regular references except for their special coercion and deduplication rules.
Note: I later found the solution proposed in this comment to be unsound and also very overcomplicated. A better solution is proposed in this reply.
Specifically:
NamedX
is directed to its destination by its namespace type N
rather than its real pointee type T
. This type must always be a concrete and 'static
type.
- If several
NamedX
instances with the same namespace are specified in the source or target CxRaw
of a coercion, the first one is always used.
- This is sound because it is illegal to construct a
CxRaw
instance with several NamedX
instances from the same namespace. Unlike with regular references with potentially generic pointees, such a requirement can always be proven always provable since N
is always a concrete 'static
type.
NamedX
structures do not need deduplication and, indeed, the fact that they do not rely on deduplication is important.
- If several
NamedX
instances with the same namespace are present in a context but some of the pointee types are still inference holes, the inference holes are all subjected to a mutual equality constraint.
(I'll clarify these rules in a bit once I start redrafting the semantics section of the proposal—there's a few other holes I have to fix anyways)
Now, consider the following code:
// We begin by defining our capabilities.
#[non_exhaustive]
struct Logger;
#[non_exhaustive]
struct Tracing;
#[non_exhaustive]
struct Database;
// Child A context
type ChildACx<'a, B> = Cx<
NamedMut<'a, Logger, <B as ChildACxBundle>::Logger>,
NamedMut<'a, Tracing, <B as ChildACxBundle>::Tracing>,
>;
trait ChildACxBundle {
type Logger: Logger;
type Tracing: Tracing;
}
impl ChildACxBundle for PhantomData<(Self::Logger, Self::Tracing)> {}
// Child B context
type ChildBCx<'a, B> = Cx<
NamedMut<'a, Logger, <B as ChildBCxBundle>::Logger>,
NamedRef<'a, Database, <B as ChildBCxBundle>::Database>,
>;
trait ChildBCxBundle {
type Logger: Logger;
type Database: Database;
}
impl ChildBCxBundle for PhantomData<Self::Logger, Self::Database> {}
// Parent context
type ParentCx<'a, B> = Cx<
ChildACx<'a, <B as ParentCxBundle>::ChildA>,
ChildBCx<'a, <B as ParentCxBundle>::ChildB>,
>;
trait ParentCx<'a> {
type ChildA: ChildACxBundle;
type ChildB: ChildBCxBundle;
}
impl ParentCx for PhantomData<(Self::ChildA, Self::ChildB)> {}
// Usage demo
type CallerCx<'a> = Cx<
NamedMut<'a, Logger, Either<Logger, Tracing>>, // N.B. these still must reference distinct instances
NamedMut<'a, Tracing, Either<Logger, Tracing>>, // because their namespaces are distinct and are therefore
// assumed to be distinct.
NamedRef<'a, Database, MyDatabase>,
>;
fn caller(cx: CallerCx<'_>) {
parent(cx);
}
fn parent<B: ParentCxBundle>(cx: ParentCx<'a, B>) {
let logger_a = child_a(cx);
let logger_b = child_b(cx);
// We cannot use both `B::ChildA::Logger` and `B::ChildB::Logger` at the same time here because, even though
// they're possibly-different generic types, they are bound to the same namespace.
// let _ = (logger_a, logger_b);
}
fn child_a<B: ChildACxBundle>(cx: ChildACx<'a, B>) -> &'a mut B::Logger {
// ... do stuff here ...
cx
}
fn child_b<B: ChildBCxBundle>(cx: ChildBCx<'a, B>) -> &'a mut B::Logger {
// ... do stuff here ...
cx
}
With this small extension to the proposal, have now the ability to:
- provide multiple instances of the same type but with different uses (notice how
Either<Logger, Tracer>
is used as the pointee for both the logger and the tracer?)
- accept an abstract component implementing a trait rather than a specific component
...without compromising the central objective that child contexts can be updated without updating their ancestor contexts.
Admittedly, this is quite verbose. Although outside of the scope of this RFC, one could imagine a syntactic sugar for type bundles or the ability to define inference holes in type aliases pairing quite well with this feature:
#[non_exhaustive]
struct Logger;
#[non_exhaustive]
struct Tracing;
#[non_exhaustive]
struct Database;
type ChildACx<'a> = Cx<
NamedMut<'a, Logger, _: Logger>,
NamedMut<'a, Tracing, _: Tracing>,
>;
type ChildBCx<'a> = Cx<
NamedMut<'a, Logger, _: Logger>,
NamedRef<'a, Database, _: Database>,
>;
type ParentCx<'a> = Cx<ChildACx<'a>, ChildBCx<'a>>;
Backwards Compatibility with Existing Traits and Implicit Passing
Unlike the effects-based proposal, this proposal does not currently allow users to inject additional context to an existing trait lacking facilities for that context passing.
It is, however, relatively easy to retrofit this functionality onto existing traits:
trait MyTrait {
type Cx<'a>: AnyCx;
fn do_something(&self, cx: Cx<'_>);
}
struct MyStruct<F> {
targets: Vec<F>,
}
impl<F: MyTrait> MyStruct<F> {
pub fn call_things(&self, cx: F::Cx<'_>) {
for target in &self.targets {
// This technically isn't possible in the current proposal yet because I never defined reborrowing
// rules for `AnyCx`. This is a mistake and should be relatively easy to add in the next draft.
targets.do_something(cx);
}
}
}
Of course, if this becomes too cumbersome, we can add implicit passing later. I'll have to think a bit more about the specifics of such a feature but I don't really see it being any more complicated than what would be required by the effects-based proposal.