hjpev
hjpev

Reputation: 700

Rust typestate pattern: implement for multiple states?

I have a struct with several states:

struct Select;
struct Insert;
struct Delete;
// ...more states

struct Query<T> {
    // ... some non-generic fields
    marker: PhantomData<T>
}

I have some functionality which I would like to implement for some, but not all of states. I imagine it should look something like this:

impl Query<T> for T: Select | Update | Delete {
    // implement only once
    fn foo() -> Query<T>;
}

Is this possible and if so, how?

Upvotes: 1

Views: 679

Answers (3)

Aleksander Krauze
Aleksander Krauze

Reputation: 6081

There are two main methods you could do that. With trait guards, as Chayim suggested, or with a macro. Let's see how each of those solutions work and what are their trade-offs.

Trait guard

This is a pretty easy concept, however it has a subtle nuances. We want to define some kind of Guard trait, implement it for some types and then leverage generic implementation. For example:

pub trait Guard {}

impl Guard for Select {}
impl Guard for Update {}
impl Guard for Delete {}

impl<T: Guard> Query<T> {
    pub fn foo() -> Query<T> {
        todo!()
    }
}

This however has an important drawback. Since Guard is a public trait if someone would implement it for some other type Other then impl<T: Guard> would apply to Other type as well. This could be undesired, as depending on your project's requirements this could lead to broken invariants.

We could try making Guard a private trait, but this currently (rustc 1.70.0, 2021 Edition) results in a warning and will become an error in the future.

warning: private trait `Guard` in public interface (error E0445)
  --> src/lib.rs:24:1
   |
24 | impl<T: Guard> Query<T> {
   | ^^^^^^^^^^^^^^^^^^^^^^^
   |
   = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
   = note: for more information, see issue #34537 <https://github.com/rust-lang/rust/issues/34537>
   = note: `#[warn(private_in_public)]` on by default

We can solve it by using a sealed trait:

mod sealed {
    pub trait Sealed {}
}

pub trait Guard: sealed::Sealed {}

impl sealed::Sealed for Select {}
impl sealed::Sealed for Update {}
impl sealed::Sealed for Delete {}

impl Guard for Select {}
impl Guard for Update {}
impl Guard for Delete {}

impl<T: Guard> Query<T> {
    pub fn foo() -> Query<T> {
        todo!()
    }
}

This however doubles number of implementations we have to write and results in slightly uglier API (since we "leak" private seal to public API). It could also result in less readable documentation, since reader must check which types implement Guard in the first place.

Macros

Alternatively you could use a declarative macro. This would result in a very similar syntax to what you described.

macro_rules! foo_impl {
    ($($state:ty),*) => {
        $(impl Query<$state> {
            pub fn foo() -> Query<$state> {
                todo!()
            }
        })*
    };
}

foo_impl!(Select, Update, Delete);

This has a couple of advantages:

  • You don't have to repeat yourself with implementing guard trait for your types.
  • You don't have to worry about someone else implementing guard trait for other types.
  • You have a cleaner API with (possibly) more readable docs.

If you on the other hand like better solution with traits you can still write a macro that would automatically implement guard and it's seal for your types.

macro_rules! guard_impl {
    ($($state:ty),*) => {
        $(
            impl sealed::Sealed for $state {}
            impl Guard for $state {}
        )*
    };
}

guard_impl!(Select, Update, Delete);

Upvotes: 2

Filipe Rodrigues
Filipe Rodrigues

Reputation: 2177

Another way is to use the duplicate crate:

// duplicate = "1.0.0"
#[duplicate::duplicate_item(
    T;
    [ Select ]; [ Update ]; [ Delete ];
)]
impl Query<T> {
    // implement only once
    fn foo() -> Query<T>;
}

This has different advantages/disadvantages compared to using a trait.

  • (Advantage) When using a trait, if you need to refer to some item defined with T (e.g. T::my_fn() or T::Type), you need to add that item to the trait and all implementations. Meanwhile when using duplicate, since you're using the types themselves, there's no need for that.
  • (Disadvantage) When using a trait, you can re-use it for different methods that need it, including "and"ing two requirements (T: TraitA + TraitB), as well as "or"ing requirements (impl trait TraitAB for A | B, T: TraitAB). With duplicate, you must specify all the states each time you define the method / block, which may lead to repetition.

Upvotes: 1

Chayim Friedman
Chayim Friedman

Reputation: 71025

Create a trait that all wanted states implement (and nothing else), and constrain where T: Trait.

Upvotes: 2

Related Questions