idanarye/rust-typed-builder

Possible rustc language features for less magic?

Opened this issue ยท 10 comments

Was just thinking which kind of language features could help with a cleaner approach compared "impl on tuple shadowing" and "sealed enums" โ€“ which is a creative way to solve it for sure ๐Ÿ‘. Are you already aware of RFCs that touch on those? Something that came to mind, but no idea if it's feasable: "Option must be Some"-attribute that makes the compiler error if option is None

struct UwuBuilder {
  #[option_must_be_some_compile_error(message = "owo is required")]
  owo: Option<usize>,
}

If an Option cannot be None - why not just make it a non-Option?

#[derive(Default)]
struct UwuBuilder {
  /// required
  #[option_must_be_some_compile_error(message = "owo is required")]
  owo: Option<usize>,

  /// optional
  uwu: Option<usize>,
}

impl UwuBuilder {
  fn owo(self, owo) -> Self {
    self.owo = Some(owo);
    self
  }

  fn uwu(self, uwu) -> Self {
    self.uwu = Some(uwu);
    self
  }

  fn build(self) {}
}

UwuBuilder::default().owo(1).uwu(2).build(); // Compiles
UwuBuilder::default().uwu(2).build(); // Does not compile, because `UwuBuilder::owo` is `None`

That would be idea. Makes more sense? But as I said, no idea if it's feasible, especially in terms of type inference. Also adjusted the initial description, as "panic" is misleading, "compiler error" was what I meant.

Generally I feel like there's a language feature hiding to help make this whole thing less magic, which would be good, right?

Why not this?

#[derive(Default)]
struct UwuBuilder {
  /// required
  owo: usize,

  /// optional
  uwu: Option<usize>,
}

Then you can't have a builder, can you? Because you would need to instantiate UwuBuilder with owo already set, which does not allow to have the builder pattern.

Oh. I see your point now - I've somehow missed that you were defining an UwuBuilder struct and not an Uwu struct. But in this crate I went for a different approach - you would define the Uwu struct and let the derive macro create the UwuBuilder, which is generic and its generic parameter encodes which fields were set already. So an UwuBuilder where owo is set has a different type (because of the generic parameter) than an UwuBuilder where owo is not set, and only the former will have a build method.

Yeah, I know how your crate is working. The issue was meant to talk about possible language features to not require the hacks that your crate is doing to enable the behavior it's providing โ€“ because generating impls on different combinations of tuples ("tuple shadowing" in the initial post) and having an enum without variants so you can't call the build function ("sealed enum" in the initial post) are hacks. Creative hacks, but hacks.

Sorry if that sounded a bit rude. And if you feel like this is out of scope for your crate specifically, feel free to close.

Well, if we are talking about language features, how about something like this?

struct UwuBuilder<const D: Definition<Self>>  {
    /// required
    owo: usize,

    /// optional
    #[omittable]
    uwu: usize,
}

impl<const D: Definition<UwuBuilder>> UwuBuilder<D> {
    fn build(self) -> Uwu {
        Uwu {
            owo: self.owo,

            #[if(D.has("uwu"))]
            uwu: self.uwu,

            #[if(!D.has("uwu"))]
            uwu: 42,
        }
    }
}

And use it like this:

assert_eq(
    UwuBuilder {
        owo: 1,
    }.build(),
    Uwu {
        owo: 1,
        uwu: 42,
    },
);

assert_eq(
    UwuBuilder {
        owo: 2,
        uwu: 3,
    }.build(),
    Uwu {
        owo: 2,
        uwu: 3,
    },
);

Interesting approach!

But isn't the tricky part of the generated builder struct that you need to have all fields be Options, so that the struct can get initialized completely empty, and then only fill the Options with Some after constructing via method calls? So your

struct UwuBuilder<const D: Definition<Self>>  {
    /// required
    owo: usize,

wouldn't work, right?

"Starting empty" is not really a core requirement of the builder pattern. One could imaging a builder that works like this:

assert_eq!(
    Uwu.builder(2).uwu(3).build(),
    Uwu { owo: 2, uwu: 3 },
);

Or even:

assert_eq!(
    UwuBuilder { owo: 2 }.uwu(3).build(),
    Uwu { owo: 2, uwu: 3 },
);