A while back, I learned about the type-state builder pattern in Rust a while ago through https://www.youtube.com/watch?v=pwmIQzLuYl0. Recently, I found a use-case in a project I am working on:
// Type-States
#[derive(Clone)]
pub struct NonBlocking;
#[derive(Clone)]
pub struct Blocking;
#[derive(Clone)]
pub struct WithoutEnv;
#[derive(Clone)]
pub struct WithEnv {
env_variables: Arc<Mutex<EnvVariables>>,
}
#[derive(Clone)]
pub struct NoOutput;
#[derive(Clone)]
pub struct WithOutput;
#[derive(Clone)]
pub struct NonInterruptible;
pub struct Interruptible {
pub interrupt_rx: Receiver<InterruptSignal>,
}
// Advantages of the Type-State Builder Pattern:
// 1. We don't have any option/enum (an alternative configuration strategy)
// checking overhead at runtime.
// 2. We can guarantee that we handled all possible Command "variants"
// (combination of config options), that we use, at compile-time.
// 3. Arguably, this also results in separated, cleaner code.
/// A Command offering customization of the blocking behaviour, the input
/// environment variables, whether the output is captured and whether the
/// execution can be interrupted. Utilizes the type-state builder pattern to
/// enforce these configurations at compile-time.
#[derive(Clone)]
pub struct CommandBuilder<B = NonBlocking, E = WithoutEnv, O = NoOutput, I = NonInterruptible> {
command: String,
blocking: B,
output: O,
interruptible: I,
env: E,
}
The implementation works well, but there is one last thing I am unsure about. The video also utilizes Zero-Sized Types (here those would be NonBlocking, Blocking, NoOutput, WithOutput etc.), but wraps them inside PhantomData
. For my example, doing that as well would mean wrapping the blocking
and output
fields in PhantomData. Is there any benefit to doing this? From what I understand, PhantomData also represents ZST and acts as though it stores the underlying type (for static analysis by the compiler), but doesn't actually. Does this mean using it would be more memory efficient? Is the issue that I am not "telling" the compiler which actual types e.g. B
can have? For instance, I know B
can only be Blocking
or NonBlocking
, both ZST types, but I don't explicitly specify anywhere that those are the only accepted types, so maybe the compiler doesn't know and therefore does give that field a non-zero size at runtime.