Search code examples
rusttraitsapi-design

Rust trait with "simple" and "advanced" versions


I have two traits which are basically equivalent, but one provides a lower level interface than the other. Given the higher level trait, one can easily implement the lower level trait. I want to write a library which accepts an implementation of either 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, State trait provides an interface where you define the .children() function to list the children, and the .cull() function to potentially cull a state.

The RawState trait provides an interface where you define a function .children_with_cull() instead, which iterates through the children and culls them in a single function call. This allows an implementation of RawState to never even generate children that it knows will be culled.

I would like to allow most users to only implement the State trait, and have the RawState implementation be automatically generated based on their State implementation. However, when implementing State, some parts of the trait are still a part of RawState, e.g.

#[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 type "Cost" is defined in RawState, not in State. On potential workaround, to redefine all the relevant parts of the RawState inside State, i.e. define 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 the ambiguous duplicate definitions. For example in the DummyState implementation for State, it will complain that Self::Cost is ambiguous, since it can't tell whether you are referring to <Self as State>::Cost, or <Self as RawState>::Cost.


Solution

  • Considering that both RawState and State are not object-safe (because they use Self in return types), I'll assume that you don't intend to create trait objects for these traits (i.e. no &RawState).

    The supertrait bound State: RawState is mostly important when dealing with trait objects, because trait objects can only specify one trait (plus a select few whitelisted traits from the standard library that have no methods, like Copy, Send and Sync). The vtable that the trait object refers to only contains pointers to the methods defined in that trait. But if the trait has supertrait bounds, then the methods from those 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 the supertrait bound is important is when the subtrait provides default implementations for some methods. The default implementation can make use of the supertrait bound to access methods from another trait.

    Since you can't use trait objects, and since you don't have default implementations for the methods in State, I think that you should simply not declare the supertrait bound State: RawState, because it adds nothing (and indeed, causes issues).

    With this approach, it becomes necessary to copy the members from RawState that we need to implement State, as you suggested. State would thus be defined like this:

    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 the bound State: Sized is required because we use Self in ChildrenIterator. RawState also need the bound RawState: Sized.)

    Finally, we can provide a blanket impl of RawState for 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 disambiguate the conflicting names: <Self as State>. It's used on the two members we duplicated so that RawState defers to State.