rusttypesbuilderinvariants

Typed builder pattern: bypassing invariants runtime


I would like to determine if there is a way to follow the type state pattern but allow invariants if there is an alternative.

Suppose I have a builder pattern for a wrapper struct around a trait.

trait Trait {
}

struct Wrapper<T: Trait> {
    inner: T
}

struct Builder<T: Trait> {
    inner: Option<T>
}

impl<T: Trait> Wrapper<T> {
    fn builder() -> Builder<T> {
        Builder {
            inner: None
        }
    }
}

impl<T: Trait> Builder<T> {
    fn with_inner(mut self, inner: T) -> Self {
        self.inner = inner;
        self
    }

    fn build(self) -> Option<Wrapper<T>> {
        Some(Wrapper {
            inner: self.inner?,
        })
    }
}

Builder::build returns an Option<Wrapper<T>> where Option::None represents that the build failed; the build was supplied with invariants i.e. invalid arguments.


I can use a typed builder pattern to prevent invariants compile time instead of runtime.

struct Invariant;

struct TypedBuilder<T> {
    inner: T
}

impl<T: Trait> Wrapper<T> {
    fn typed_builder() -> TypedBuilder<Invariant> {
        TypedBuilder {
            inner: Invariant
        }
    }
}

impl<T> TypedBuilder<T> {
    fn with_inner<T2: Trait>(self, inner: T2) -> TypedBuilder<T2> {
        TypedBuilder {
            inner: inner
        }
    }
}
impl<T: Trait> TypedBuilder<T> {
    fn build(self) -> Wrapper<T> { // infallible
        Wrapper {
            inner: self.inner,
        }
    }
}

In this example, I would like TypeBuilder's inner to default T if it is defaultable.

So the invariants would be:

I don't know if such a design pattern is possible without nightly features such as trait specialization.


Solution

  • The solution was to use a second generic to track the target type to build, then allow the invariant if the target type is Default.

    See below playground:

    use std::marker::PhantomData;
    
    trait Trait {}
    
    struct Wrapper<T: Trait> {
        inner: T
    }
    
    struct Invariant;
    
    struct TypedBuilder<Target, T> {
        inner: T,
        marker: PhantomData<Target>
    }
    
    impl<T: Trait> Wrapper<T> {
        fn typed_builder() -> TypedBuilder<T, Invariant> {
            TypedBuilder {
                inner: Invariant,
                marker: PhantomData,
            }
        }
    }
    
    impl<Target, T> TypedBuilder<Target, T> {
        fn with_inner<T2: Trait>(self, inner: T2) -> TypedBuilder<Target, T2> {
            TypedBuilder {
                inner: inner,
                marker: self.marker,
            }
        }
    }
    
    impl<T: Trait> TypedBuilder<T, T> {
        fn build(self) -> Wrapper<T> { // infallible
            Wrapper {
                inner: self.inner,
            }
        }
    }
    
    impl<T: Trait + Default> TypedBuilder<T, Invariant> {
        fn build(self) -> Wrapper<T> { // infallible
            Wrapper {
                inner: T::default(),
            }
        }
    }
    
    #[derive(Default)]
    struct Foo;
    struct Bar;
    
    impl Trait for Foo {}
    impl Trait for Bar {}
    
    fn main() {
        let _foo: Foo = Wrapper::<Foo>::typed_builder()
            .build().inner;
        let _foo: Foo = Wrapper::<Foo>::typed_builder()
            .with_inner(Foo)
            .build().inner;
           
        // Can't compile since Bar is not supplied, and is not default 
        // let _bar: Bar = Wrapper::<Bar>::typed_builder()
        //     .build().inner;
        let _bar: Bar = Wrapper::<Bar>::typed_builder()
            .with_inner(Bar)
            .build().inner;
    }