Open johannescpk opened 2 years ago
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 Option
s, so that the struct can get initialized completely empty, and then only fill the Option
s 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 },
);
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 beSome
"-attribute that makes the compiler error if option isNone