简体   繁体   中英

How to implement an introspection of a trait content?

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

First of all, Rust is not a dynamic language, so after compilation introspection is not possible unless you are prepared for that. Basically you have to modify your Plan type and your library in some way to support external introspection.

The options are that either you expose all the fields as public, so that you can go over them from an external crate function:

pub SequencePlan {
    pub first: ...,
    pub second: ...,

Or you have a Visitor-like trait inside the library that goes over the structure for you, and with that you can externally get all of the structure's details.

You can roll own your own Visitor, but having a generic visitor is not a basic task. You need to decide on a common output type for all Plan subtypes, and that type needs to cover all the possible use cases.

In Rust the general case introspection is done in a library serde which is typically used for serialization/conversion.

PS It is possible to do something like dynamic_cast in Rust, but it will be unsafe , and so this is typically not what you'd want to do.

I am posting here the code I finally wrote after @battlmonstr answer.

Plan uses erased_serde::Serialize as a supertrait, but serde::Serialize also needs to be implemented as specific plans incorporates dyn Plan trait objects:

use serde::{Serialize, Serializer};

// Generic plan infrastructure

pub trait Plan<T, R>: erased_serde::Serialize {
    fn step(self: Box<Self>, arg: T) -> StepResult<R>;
}

impl<T, R: Serialize> Serialize for dyn Plan<T, R> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        erased_serde::serialize(self, serializer)
    }
}

In most cases one can use #[derive(Serialize)] to derive the implementation. Plan implementations require Serialize for type parameters:

/// Sequence plan concatenates two underlying plans via middle value of T2 type.
#[derive(Serialize)]
pub struct SequencePlan<T1, T2, R> {
    first: Box<dyn Plan<T1, T2>>,
    second: Box<dyn Plan<T2, R>>,
}

impl<T1: 'static + Serialize, T2: 'static + Serialize, R: 'static + Serialize> 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,
            }),
        }
    }
}

Sometimes custom implementation is required:

/// Operator from T to StepResult<R> is a plan which calls itself when executed
pub struct OperatorPlan<T, R> {
    operator: Box<dyn FnOnce(T) -> StepResult<R>>,
    descr: String,
}

impl<T: Serialize, R: Serialize> Plan<T, R> for OperatorPlan<T, R> {
    fn step(self: Box<Self>, arg: T) -> StepResult<R> {
        (self.operator)(arg)
    }
}

impl<T: Serialize, R: Serialize> Serialize for OperatorPlan<T, R> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.descr.as_str())
    }
}

Finally one can write a method to serialize a plan into a string:

fn plan_to_string<R>(step: &Box<dyn Plan<(), R>>) -> String {
    let mut buf = Vec::new();
    let json = &mut serde_json::Serializer::new(Box::new(&mut buf));
    let serializer = &mut <dyn erased_serde::Serializer>::erase(json);
    step.erased_serialize(serializer).unwrap();
    std::str::from_utf8(buf.as_slice()).unwrap().to_string()
}

Full code is at the playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM