idanarye / rust-typed-builder

Compile-time type-checked builder derive
https://crates.io/crates/typed-builder
Apache License 2.0
925 stars 52 forks source link

Possible rustc language features for less magic? #71

Open johannescpk opened 2 years ago

johannescpk commented 2 years ago

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 :+1:. 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>,
}
idanarye commented 2 years ago

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

johannescpk commented 2 years ago
#[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?

idanarye commented 2 years ago

Why not this?

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

  /// optional
  uwu: Option<usize>,
}
johannescpk commented 2 years ago

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.

idanarye commented 2 years ago

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.

johannescpk commented 2 years ago

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.

johannescpk commented 2 years ago

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.

idanarye commented 2 years ago

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,
    },
);
johannescpk commented 2 years ago

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?

idanarye commented 2 years ago

"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 },
);