I've got code that looks like this (renamed & simplified):
trait CoreTask {
type Dependencies;
type Output;
fn execute_with(self, dependencies: Self::Dependencies) -> impl Future<Output = Self::Output>;
}
mod core_tasks {
pub struct CreateFoo { /* ... */ }
impl CoreTask for CreateFoo { /* ... */ }
pub struct GetBars { /* ... */ }
impl CoreTask for CreateFoo { /* ... */ }
// ...
}
pub struct UiTask<T: CoreTask, Message> {
core_task: T,
to_message: Box<dyn Fn(T::Output) -> Message>,
}
pub enum Message { /* ... */ }
pub enum Task {
CreateFoo(UiTask<core_tasks::CreateFoo, Message>),
GetBars(UiTask<core_tasks::GetBars, Message>),
// ...
}
fn do_some_work() {
let task: Task = get_task();
match task {
CreateFoo(UiTask { core_task, to_message }) => /* ... */,
GetBars(UiTask { core_task, to_message }) => /* ... */,
// ...
}
As you can see, there's a lot of boilerplate. Unless there's a way to remove the generic parameter T
from UiTask
and use a trait object for the core_task
field—and after two days of headaches, I am forced to admit there isn't—I must use an enum.
Is there a way to cut down on the boilerplate? I found the enum_dispatch
and enum_delegate
crates, but they work on enums whose variant stores implementors of a trait, whereas UiStruct
is more than that (and the enum can't implement the trait regardless, because associated types of CoreTask
differ across variants).
The match_any
was helpful in cutting down on the final match statement by letting me treat the returned task as if it were a trait object, but that leaves the rest of the boilerplate untouched, and it is not itself without drawbacks, as rust-analyzer breaks inside the macro.
Is it hopeless?