Search code examples
genericsruststate-pattern

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?


Solution

  • 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);