I implemented a small library to make calculations step by step by modifying plan incrementally. I would like to allow making an introspection of the plans without modifying the library itself. For instance I need implementing a function which prints next plan after each step of execution. Or I may need to convert a plan into another representation.
The central abstraction of the library is a Plan<T, R>
trait which inputs an
argument T
and calculates R
:
pub trait Plan<T, R> {
fn step(self: Box<Self>, arg: T) -> StepResult<R>;
}
Plan returns StepResult<R>
which is either an immediate result or new plan
from ()
to R
:
pub enum StepResult<R> {
Plan(Box<dyn Plan<(), R>>),
Result(R),
}
Finally I have few specific plans, for example these two:
pub struct OperatorPlan<T, R> {
operator: Box<dyn FnOnce(T) -> StepResult<R>>,
}
impl<T, R> Plan<T, R> for OperatorPlan<T, R> {
fn step(self: Box<Self>, arg: T) -> StepResult<R> {
(self.operator)(arg)
}
}
pub struct SequencePlan<T1, T2, R> {
first: Box<dyn Plan<T1, T2>>,
second: Box<dyn Plan<T2, R>>,
}
impl<T1: 'static, T2: 'static, R: 'static> Plan<T1, R> for SequencePlan<T1, T2, R> {
fn step(self: Box<Self>, arg: T1) -> StepResult<R> {
match self.first.step(arg) {
StepResult::Plan(next) => StepResult::plan(SequencePlan{
first: next,
second: self.second,
}),
StepResult::Result(result) => StepResult::plan(ApplyPlan{
arg: result,
plan: self.second,
}),
}
}
}
Using the plans I can combine operators and calculate R
from T
step by step
building a plan incrementally.
I have read answers to How do I create a heterogeneous collection of objects? But both "trait" and "enum" solutions doesn't work to me.
I could add new function like fmt
or convert
into Plan<T, R>
trait each
time but the goal is to provide a single function to allow introspection
without modifying the library itself.
I cannot list all plan types as a enum because some of them (like
SequencePlan
) are generics and thus OperatorPlan
can return a Plan
which
exact type is not known in advance.
I tried implementing a Visitor pattern by adding new trait Visitor<T, R>
and
method to accept it into Plan<T, R>
trait:
pub trait Plan<T, R> {
fn step(self: Box<Self>, arg: T) -> StepResult<R>;
fn accept(&self, visitor: &Box<dyn PlanVisitor<T, R>>);
}
trait PlanVisitor<T, R> {
fn visit_operator(&mut self, plan: &OperatorPlan<T, R>);
// Compilation error!
//fn visit_sequence<T2>(&mut self, plan: &SequencePlan<T, T2, R>);
}
This doesn't compile because function visiting SequencePlan
is parameterized
by additional type. On the other hand I don't need to know the full type of the
Plan
to print it.
In C++ I could use dynamic_cast<Display>
to see if Plan
is printable and
use the pointer to Display
interface after. I know that Rust doesn't support
downcasting out of the box.
I would like to know what is a natural way to implement such introspection in Rust?
More complete code on playground