Builder Pattern that doesn't set a field twice

Hi folks,

I started using a simplified version of the Builder Pattern (I don't use the second FooBuilder struct).

The idea is that I don't set all fields since some of those set functions might be resource-intensive and not all fields are always needed.

It works great, but now I need to add a field that needs to be Option<T> and Option<T> is already used to identify fields that haven't been set yet.

I could use Option<Option<T>> but that doesn't feel right. Are there any best practices for that?

Thank you

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Color {
    Red,
    Green,
    Blue,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct MyStruct {
    name: Option<String>,
    size: Option<u16>,
    color: Option<Color>,
}
impl MyStruct {
    fn new() -> Self {
        Self::default()
    }
    fn set_name(mut self, name: &str) -> Self {
        if self.name == None {
            self.name = Some(name.to_owned());
        }
        self
    }
    fn set_size(mut self, size: u16) -> Self {
        if self.size == None {
            self.size = Some(size);
        }
        self
    }
    fn set_color(mut self, color: Color) -> Self {
        if self.color == None {
            self.color = Some(color);
        }
        self
    }
}

fn main() {
    let out = MyStruct::new().set_name("foo");
    println!("{:?}", &out);

    let out = out.set_size(123);
    println!("{:?}", &out);

    let out = out.set_size(333);
    println!("{:?}", &out);

    let out = out.set_color(Color::Green);
    println!("{:?}", &out);

    // MyStruct { name: Some("foo"), size: None, color: None }
    // MyStruct { name: Some("foo"), size: Some(123), color: None }
    // MyStruct { name: Some("foo"), size: Some(123), color: None }
    // MyStruct { name: Some("foo"), size: Some(123), color: Some(Green) }
}

What's wrong with Option<Option<T>>? I don't see why this shouldn't be used. There's even the Option::flatten method to make working with Option<Option<T>> more ergonomic.

Alternatively you could for example define your own Option-like enum with a third variant that indicates that the field hasn't been set yet:

enum Maybe<T> {
    Some(T),
    None,
    Unset,
}

and implement conversion methods or traits to and from Option<T> for it:

impl<T> Maybe<T> {
    fn from_option(o: Option<T>) -> Self {
        match o {
            Some(x) => Self::Some(x),
            None => Self::None,
        }
    }

    fn into_option(self) -> Option<T> {
        match self {
            Self::Some(x) => Some(x),
            Self::None => None,
            Self::Unset => panic!("value unset, unable to convert to `Option`"),
        }
    }
}
4 Likes

Good tip!

Awesome! Thank you!

:+1: for the Option<Option<T>>, I don't see anything wrong with that either.

Just as a quick tip to make builders a bit safer to use (not setting fields twice, not forgetting to set required fields, etc): you might want to take a look at the typestate pattern, it can make builders quite ergonomic to use :slight_smile: - can be a bit of a hassle to implement tho, so pick your poison

1 Like

If you want to look at an external crate to derive builders, I recommend bon.

It uses the typestate pattern to prevent errors like calling a builder method twice or missing an optional field.

If you need to distinguish between None (haven't set the field) and None (field set to None), Option<Option<T>> works well because the latter None is actually Some(None). Might feel weird but it's not wrong!

1 Like