Type constructor to hide implementation details of typestate builder

Continuing the discussion from Builder pattern in Rust: self vs. &mut self, and method vs. associated function:

I used that pattern in mmtkbdb::DbOptions:

pub struct DbOptions<K: ?Sized, V: ?Sized, C, N> { /* private fields */ }

[…]

The fourth type parameter determines if a name has been set (or the unnamed database has been selected), or if a name (or unnamed database) must be selected yet.

Once a database name has been selected with DbOptions::name, type argument N will be Option<CString>, but I consider this being an implementation detail.

I now have a function like this, which returns DbOptions with N set to a type (Option<CString>) indicating that a name has been set:

fn counter_db_opts(prefix: &str) -> DbOptions<str, [u8], KeysUnique, Option<CString>> {
    DbOptions::new()
        .key_type::<str>()
        .value_type::<[u8]>()
        .name(format!("{prefix}_counter"))
}

I would prefer to write the return type as FinishedDbOptions<str, [u8], KeysUnique>, which would hide the implementation detail of using a CString internally.

So I would need a type constructor that turns the type triplet <K: ?Sized, V: ?Sized, C> into DbOptions<K, V, C, Option<CString>>.

How can I express such type constructors in Rust most easily? Using traits and associated types? Using macros?

I tried to simplified the above described use case in the following Playground:

#![allow(dead_code)]

pub mod db {
    use std::ffi::CString;
    use std::marker::PhantomData;
    pub struct Builder<T, C> {
        phantom: PhantomData<fn(T) -> T>,
        conn_str: C,
    }
    impl<T> Builder<T, ()> {
        pub fn new() -> Self {
            Builder {
                phantom: PhantomData,
                conn_str: (),
            }
        }
    }
    impl<T, C> Builder<T, C> {
        pub fn conn_str(self, s: &str) -> Builder<T, CString> {
            Builder {
                phantom: PhantomData,
                conn_str: CString::new(s).expect("invalid conn_str"),
            }
        }
    }
    // Only a `Builder` with `CString` as second type argument
    // can be used for building
    impl<T> Builder<T, CString> {
        pub fn build(self) {
            todo!()
        }
    }
    // But that is an implementation detail.
    // Would it make sense to create a type constructor
    // that maps `T` to `Builder<T, CString>`?
    //
    // How to do it? Could we do it like this:
    pub trait ReadyBuilder<T> {
        type Ty;
    }
    impl<T> ReadyBuilder<T> for () {
        type Ty = Builder<T, CString>;
    }
}

// Here we need to know about the implementation detail:
fn standard_builder1<T>() -> db::Builder<T, std::ffi::CString> {
    db::Builder::new().conn_str("standard_db")
}

// Here we used the `db::ReadyBuilder` type constructor
// to hide the detail that `CString` is used as 2nd type arg:
fn standard_builder2<T>() -> <() as db::ReadyBuilder<T>>::Ty {
    db::Builder::new().conn_str("standard_db")
}

(Playground)

In that Playground, I make the module db define a trait ReadyBuilder, which serves together with its implementation on a dummy type () as a type constructor which maps a T to a Builder<T, CString>. The syntax is a bit awkward as I have to write:

<() as db::ReadyBuilder<T>>::Ty

Is this idiomatic? Which other alternatives do I have to create such a type constructor? Or should I avoid such things and not try to hide that implementation details from my module's users? Or should I avoid the typestate builder pattern overall?


I guess an alternative would be to use a struct like this:

    // Or is this better?
    pub trait TyCon {
        type Ty;
    }
    pub struct ReadyBuilderStrct<T> {
        phantom: PhantomData<T>,
    }
    impl<T> TyCon for ReadyBuilderStrct<T> {
        type Ty = Builder<T, CString>;
    }

Which then is used like this:

fn standard_builder3<T>() -> <db::ReadyBuilderStrct<T> as db::TyCon>::Ty {
    db::Builder::new().conn_str("standard_db")
}

(Playground)

But it feels more verbose even (though perhaps semantically more correct).

Oh, I have been thinking too complicated. It's easier than I thought:

    pub type ReadyBuilder<T> = Builder<T, CString>;

Which is simply used like this:

// Here we used the `db::ReadyBuilder` type constructor
// to hide the detail that `CString` is used as 2nd type arg:
fn standard_builder4<T>() -> db::ReadyBuilder<T> {
    db::Builder::new().conn_str("standard_db")
}

(Playground)

Sorry for my confusion. The keyword type can actually define types as well as type constructors (when type arguments are used).

Nit: it's a type alias (and... type alias constructor, I guess, heh).

I used that solution here:

pub type DbSpec<K, V, C> = DbBuilder<K, V, C, Option<CString>>;

So instead of writing DbBuilder<K, V, C, Option<CString>>, I can now write DbSpec<K, V, C>, e.g. as the return value of DbBuilder::name.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.