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.
- 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 - 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. - Use a associated trait object
type DynContext = Box<dyn Context>
instead of "associated trait"
a. No. The whole point of abstracting aContext
trait is to avoid runtime overheads. - Use a enum to encapsulate the function call parameters of
Context
. Therefore there would be no need for aContext
trait.
a. A large variant in enum still causes a waste of space