Characterization of rust with "simple" and "advanced" versions

I have two traits that are mostly equivalent, but one provides a lower level interface than the other. Given a higher level trait, it is easy to implement a lower level trait. I want to write a library that accepts an implementation of any trait.

My specific case is a trait for traversing a tree:

// "Lower level" version of the trait
pub trait RawState {
    type Cost: std::cmp::Ord + std::ops::Add<Output = Self::Cost> + std::marker::Copy;
    type CulledChildrenIterator: Iterator<Item = (Self, Self::Cost)>;
    fn cull(&self) -> Option<Self::Cost>;
    fn children_with_cull(&self) -> Self::CulledChildrenIterator;
}
// "Higher level" version of the trait
pub trait State: RawState {
    type ChildrenIterator: Iterator<Item = (Self, Self::Cost)>;
    fn children(&self) -> Self::ChildrenIterator;
}

// Example of how RawState could be implemented using State
fn state_children_with_cull<S: State> (s: S)
     -> impl Iterator<Item = (S, S::Cost)> 
{
    s.children()
      .filter_map(move |(state, transition_cost)|
         state.cull().map(move |emission_cost|
            (state, transition_cost + emission_cost)
         )
      )
}

      

Here the status tag provides an interface where you define a .children () function to render children, and a function .cull()

to potentially fetch state.

A function RawState

provides an interface where, instead, you define a function .children_with_cull()

that iterates through the children and discards them in a single function call. This allows the implementation RawState

to never generate children that it knows will be discarded.

I would like to allow most users to implement just the tag State

, and the implementation RawState

will be automatically generated based on the implementation of their state. However, when implemented, State

some parts of the feature are still part RawState

, for example.

#[derive(Clone, Eq, PartialEq, Hash, Debug)]
struct DummyState {}

impl State for DummyState {
    type Cost = u32;
    type ChildrenIterator = DummyIt;
    fn emission(&self) -> Option<Self::Cost> {
        Some(0u32)
    }
    fn children(&self) -> DummyIt {
        return DummyIt {};
    }
}

      

Will give errors because the "Cost" type is defined in RawState, not in the state. About a possible workaround to override all relevant parts of RawState inside the state, i.e. define the state as

pub trait State: RawState {
    type Cost: std::cmp::Ord + std::ops::Add<Output = Self::Cost> + std::marker::Copy;
    type ChildrenIterator: Iterator<Item = (Self, Self::Cost)>;
    fn cull(&self) -> Option<Self::Cost>;
    fn children(&self) -> Self::ChildrenIterator;
}

      

But then the compiler will complain about ambiguous duplicate definitions. For example, in an implementation DummyState

for, State

it will complain, which Self::Cost

is ambiguous because it cannot tell if you are referring to <Self as State>::Cost

or <Self as RawState>::Cost

.

+3


source to share


1 answer


Given that both RawState

and State

are not object-safe (because they are used Self

in the reverse types), I assume you're not going to create objects for signs of these symptoms (ie the no &RawState

).

Sutra Strait State: RawState

is mostly relevant when working with feature objects, because feature objects can only specify one feature (plus the highlighted multiple whitelists from the standard library that don't have methods like Copy

, Send

and Sync

). The vtable referenced by the attribute object contains only pointers to the methods defined in that attribute. But if this trait has overboundedness constraints, then methods from these traits are also included in the vtable. Thus, a &State

(if it was legal) would give you access to children_with_cull

.

Another situation where supratra binding is important is when subtitles provide default implementations for some methods. The default implementation can use supertrait associated with accessors from another trait.

Since you can't use object objects, and since you don't have default implementations for methods in State

, I think you just shouldn't declare sutra-strait State: RawState

because it doesn't add anything (and does cause problems).

With this approach, it becomes necessary to copy the elements from RawState

that we need to implement State

as you suggested. State

will thus be defined as follows:



pub trait State: Sized {
    type Cost: std::cmp::Ord + std::ops::Add<Output = Self::Cost> + std::marker::Copy;
    type ChildrenIterator: Iterator<Item = (Self, Self::Cost)>;

    fn cull(&self) -> Option<Self::Cost>;
    fn children(&self) -> Self::ChildrenIterator;
}

      

(Note that a link is required State: Sized

because we are using Self

c ChildrenIterator

. A RawState

border is also needed RawState: Sized

.)

Finally, we can provide cover impl

for RawState

all types that implement State

. With this, impl

any type that implements State

will automatically implement RawState

.

impl<T> RawState for T
where
    T: State
{
    type Cost = <Self as State>::Cost;
    type CulledChildrenIterator = std::iter::Empty<(Self, Self::Cost)>; // placeholder

    fn cull(&self) -> Option<Self::Cost> { <Self as State>::cull(self) }
    fn children_with_cull(&self) -> Self::CulledChildrenIterator {
        unimplemented!()
    }
}

      

Note the syntax to resolve conflicting names <Self as State>

. It was used for two elements that we duplicated to RawState

undo State

.

+5


source







All Articles