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
.
source to share
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
.
source to share