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")
}
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")
}
But it feels more verbose even (though perhaps semantically more correct).