Anything similar to associated trait of trait?

Recently I was hit with a pattern that has emerged when implementing a profiling mechanism. And it seemingly requires "associated trait" in trait.

Imaging there is an execution context with plenty of internal hooks exposed for business logics

struct Context {/* ... */}
impl Context {
    pub fn action1(&mut self) -> bool;
    pub fn action2(&mut self);
}

fn logic(context: &mut Context) {
    let res = context.action1();
    if res {
        context.action2();
    }
}

fn core_loop() {
// ..........
     logic(&mut context);
// ..........
}

Now we want to occasionally perform some profiling for every logic we run. But just occasionally, and we do not want any overhead on non-profiling executions. A naturally solution would be

pub trait Context {
    pub fn action1(&mut self) -> bool;
    pub fn action2(&mut self);
}

struct ContextImpl {/*...*/};
struct ContextProfiler {/*.....*/}
impl Context for ContextImpl {/*...*/}
impl Context for ContextProfiler {/*...*/}

// Utilizing generics to generate two copies of logic. 
// One for the real context and one for profilers
fn logic(context: &mut impl Context) {/* ... */}

fn core_loop() {
// ..........
    if should_profile{
        logic(&mut context_profiler);
    } else {
        logic(&mut context);
    }
// ..........
}

Now things get ugly, instead of a single class of context and a single class of logic, there are multiple of classes of contexts and we wish to share the common logics between them.

pub trait Exec {
    trait Context; // A hypothetical associated trait syntax.
    type ContextImpl: Self::Context;
    type ContextProfiler: Self::Context;
    fn new_context() -> Self::ContextImpl;
    fn new_profiler() -> Self::ContextProfiler;
    fn logic(context: &mut impl Self::Context); // The real meat is here
}

fn core_loop<E: Exec>() {
    let mut context = E::new_context();
    let mut context_profiler = E::new_profiler();
    // ..........
    if should_profile{
        E::logic(&mut context_profiler);
    } else {
        E::logic(&mut context);
    }
    // ..........
}

Well there are already some workarounds in my mind, but neither of them seemed perfect.

  1. Just implement two variants of logic, one for real context and one for profiler.
    a. Well, the logic may be long and complex. And you will be repeating a lot of the code
  2. Just implement two variants of logic by macros
    a. Well, it works. At the cost of significantly worse dev experience. Since you would be writing long and complex logics in macros.
  3. Use a associated trait object type DynContext = Box<dyn Context> instead of "associated trait"
    a. No. The whole point of abstracting a Context trait is to avoid runtime overheads.
  4. Use a enum to encapsulate the function call parameters of Context. Therefore there would be no need for a Context trait.
    a. A large variant in enum still causes a waste of space

Have you considered the type parameter here?

pub trait Exec<C: Context> {
  ...
  fn logic(context: &mut C);
}

Well the previous idea is to use generics to generate two copies of logic using only one declaration. One for zero-cost normal execution and one for profiling. And sorry if I didn't get what exactly you mean, I did not see how this can generate two copies of logic without impl-ing Exec<C> twice.

Rust Playground

pub trait Exec {
    fn new_context() -> ContextImpl;
    fn new_profiler() -> ContextProfiler;
    fn logic(context: &mut impl Context);
    // fn logic<C: Context>(context: &mut C); // or this
}

This is the same method as in the second code snippets in my post. The problem is that there will be a lot of different Contexts to abstract over. To put into perspective, the stuff we are trying to abstract over, in its essence, looks like something as follows.

pub trait Context1 {/*...*/}
pub struct ContextImpl1 {/*...*/}
pub struct ContextProfiler1 {/*...*/}
impl Context1 for ContextImpl1 {/*...*/}
impl Context1 for ContextProfiler1 {/*...*/}
fn logic1(context: &mut impl Context1) {/* ... */}

pub trait Context2 {/*...*/}
pub struct ContextImpl2 {/*...*/}
pub struct ContextProfiler2 {/*...*/}
impl Context2 for ContextImpl2 {/*...*/}
impl Context2 for ContextProfiler2 {/*...*/}
fn logic2(context: &mut impl Context2) {/* ... */}

//...

Then you have to share a complete minimal example to show the real pain point instead of saying it'd be verbose or it'd be complex or it'd not be zero cost or sharing some useless invalid snippets.

2 Likes