简体   繁体   English

如何实现对特征内容的自省?

[英]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.例如,我需要实现一个 function,它在每个执行步骤后打印下一个计划。 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 :库的中心抽象是一个Plan<T, R>特征,它输入一个参数T并计算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 :计划返回StepResult<R>是立即结果或从()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.使用这些计划,我可以组合运算符并从T逐步计算R逐步构建计划。

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.我可以像fmt一样添加新的 function 或每次convertPlan<T, R>特征,但目标是提供单个 function 以允许在不修改库本身的情况下进行自省。

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.我不能将所有计划类型列为枚举,因为其中一些(如SequencePlan )是 generics ,因此OperatorPlan可以返回一个预先不知道确切类型的Plan

I tried implementing a Visitor pattern by adding new trait Visitor<T, R> and method to accept it into Plan<T, R> trait:我尝试通过添加新特征Visitor<T, R>和将其接受到Plan<T, R>特征中的方法来实现访问者模式:

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.这不会编译,因为访问SequencePlan的 function 是由附加类型参数化的。 On the other hand I don't need to know the full type of the Plan to print it.另一方面,我不需要知道Plan的完整类型来打印它。

In C++ I could use dynamic_cast<Display> to see if Plan is printable and use the pointer to Display interface after.在 C++ 中,我可以使用dynamic_cast<Display>查看Plan是否可打印,然后使用指向Display接口的指针。 I know that Rust doesn't support downcasting out of the box.我知道 Rust 不支持开箱即用的向下转换。

I would like to know what is a natural way to implement such introspection in Rust?我想知道在 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.首先,Rust 不是动态语言,所以编译后的反省是不可能的,除非你准备好了。 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:选项是您将所有字段公开为公共,以便您可以从外部板条箱 function 对它们进行 go:

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.您需要为所有 Plan 子类型确定一个常见的 output 类型,并且该类型需要涵盖所有可能的用例。

In Rust the general case introspection is done in a library serde which is typically used for serialization/conversion.在 Rust 中,一般情况自省是在通常用于序列化/转换的库serde中完成的。

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. PS 可以在 Rust 中执行类似dynamic_cast的操作,但它会是不安全的,因此这通常不是您想要做的。

I am posting here the code I finally wrote after @battlmonstr answer.我在这里发布了我在@battlmonstr 回答后最终编写的代码。

Plan uses erased_serde::Serialize as a supertrait, but serde::Serialize also needs to be implemented as specific plans incorporates dyn Plan trait objects: Plan 使用erased_serde::Serialize作为超特征,但serde::Serialize也需要实现,因为具体计划包含dyn Plan trait 对象:

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.在大多数情况下,可以使用#[derive(Serialize)]来派生实现。 Plan implementations require Serialize for type parameters: Plan实现需要对类型参数进行Serialize

/// 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完整代码在操场

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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