-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Typesafe builders #33
Comments
Ohh, that looks interesting! IIUC, you define a new struct with the same field names, but each field has a type parameter that is of type This is a far nicer version of the Optional Builder described in #32 (comment). Also, I love session types! I'm hoping we can land #32 soon-ish. A PR building on that would be great. What do you think, @colin-kiegel? @cramertj, what API are you envisioning? I assume you want to expand #[derive(Builder)]
#[setters(fancy_name_here)]
struct Foo {
bar: i32,
baz: i32,
} to something like struct Foo {
bar: i32,
baz: i32,
}
mod foo_builder {
struct FooBuilderFieldUnset;
struct FooBuilder<T0 = FooBuilderFieldUnset, T1 = FooBuilderFieldUnset> {
bar: T0,
baz: T1,
}
impl<T0, T1> FooBuilder<Unset, T1> {
fn bar(self, bar: i32) -> FooBuilder<i32, T1> {
FooBuilder {
bar: bar,
baz: self.baz,
}
}
}
impl<T0, T1> FooBuilder<T0, Unset> {
fn baz(self, baz: i32) -> FooBuilder<T0, i32> {
FooBuilder {
bar: self.bar,
baz: self.baz,
}
}
}
impl FooBuilder<i32, i32> {
fn build(self) -> Foo<i32, i32> {
Foo {
bar: self.bar,
baz: self.baz,
}
}
}
}
use foo_builder::FooBuilder; |
This is definitely an intersting idea and generating the code should be no problem. IMO the most critical part is the API - since there are ideas/requests to extend this crate into very different directions. I think we should be careful here not to end up in a messy situation. Let's say we have these different 'modes'
I assume those modes would be mutually exclusive. On top of that we have additional options and features and I wonder if all of these have the same meaning or any meaning for all of these modes. I would like to go through as many combinations as we can think of beforehand:
So basically what I would like to do is: Take all settings from the simplified builder (SB) case and define their meaning in the TB-cases and vice-versa. Does that make sense to you? If anyone wants to start a suggestion please go ahead. You can find the intermediate settings documentation of the macros 1.1 re-implementation here. :-) |
Great points, @colin-kiegel. The API surface might indeed get quite large if we keep on adding new shiny things. Let me think about that for a minute. Regarding getters: Are these part of the builder pattern? It makes sense to have them to avoid leaking the internal names of fields (or even to conversions), but it might be an option to not offer them in this crate. The accessors crate might be a good alternative. It's name suggests generating 'simple' methods (getters, setters). Personally, I want to make sure we generate code that is useful to a lot of people in practice and that tries to be as good or better as the code you'd write yourself. The simple setters we currently generate are nice, and try to be a bit more clever than what you'd write yourself (using Sadly, only implementing typesafe builders is not really an option, as overwriting set values can be a valid use case (e.g. when passing partially built builders around). Also, debugging them will require some deeper understanding (missing method errors with a lot of constraints on a lot of type parameters). Hybrid approaches might also be interesting. Not restricting fields to only be set once is just a matter of loosening some constraints. Keeping the final tl;dr Let's get rid of 'simple' builders in favor of an accessor crate, and implement traditional or typesafe builders or a mix of both. |
Personally, I think that |
This is a fun discussion. @cramertj Hm, I think the main traits we can implement are On top of that we could derive a I guess that's as far as we will get with abstracting the builder pattern or anything else into traits here. @killercup ok, let's go orthogonal to the accessors crate and not try to do to much at once. We could even get in touch with the maintainers of the accessors crate and try to sync our APIs to some degree (prefixes, skipping fields, etc.). I think it would be cool if these could feel like siblings. But it's no deal breaker if not. In that perfect synced world, we would advocate to use the accessors crate for deriving getters on We should then use a different namespacing for our attributes, e.g. Can we argue similarly with CustomDefaults? We already said this should be something separate. Suppose there is a crate for that, which I am sure there will be soon enough, if not already. We don't need to implement CustomDefaults for If typesafety is a global property, we could indeed promote it to the level of the derive entry point I see some trouble in deriving a TypesafeBuilder for a generic That's it for now. |
This would be much easier to implement I suspect if the builder had two states, expressed as session types:
A custom method to initialize the builder could demand all the required fields and then return For cases where the caller can't invoke that method, a second method To avoid an absolute explosion of types, I think we'd need to figure out a way to export this state trait and the two empty enums once, directly from the crate. |
Hi! I just bounced on this issue Googling for the exact thing you guys are trying here. I am not using the crate, but I may want to file some Rust RFC. I think you guys may be interested in this little syntactic thing. Consider my current code: pub struct EdgeBuilder<F, T> {
from: F,
to: T,
}
impl Default for EdgeBuilder<(), ()> {
fn default() -> EdgeBuilder<(), ()> {
EdgeBuilder {
from: (),
to: (),
}
}
}
impl<F, T> EdgeBuilder<F, T> {
pub fn from(self, vertex: &Vertex) -> EdgeBuilder<[u8; 32], T> {
let EdgeBuilder {
from, to
} = self;
EdgeBuilder {
from: *vertex.owner.bytes(),
to,
}
}
pub fn to(self, vertex: &Vertex) -> EdgeBuilder<F, [u8; 32]> {
let EdgeBuilder {
from, to
} = self;
EdgeBuilder {
to: *vertex.owner.bytes(),
from,
}
}
}
impl EdgeBuilder<[u8; 32], [u8; 32]> {
pub fn build(self) -> ::crypto::Result<Edge> {
Err(error::Unspecified)
}
} (written by hand), versus the code I would want to write: pub struct EdgeBuilder<F, T> {
from: F,
to: T,
}
impl Default for EdgeBuilder<(), ()> {
fn default() -> EdgeBuilder<(), ()> {
EdgeBuilder {
from: (),
to: (),
}
}
}
impl<F, T> EdgeBuilder<F, T> {
pub fn from(self, vertex: &Vertex) -> EdgeBuilder<[u8; 32], T> {
EdgeBuilder {
from: *vertex.owner.bytes(),
.. self
}
}
pub fn to(self, vertex: &Vertex) -> EdgeBuilder<F, [u8; 32]> {
EdgeBuilder {
to: *vertex.owner.bytes(),
.. self
}
}
}
impl EdgeBuilder<[u8; 32], [u8; 32]> {
pub fn build(self) -> ::crypto::Result<Edge> {
Err(error::Unspecified)
}
} The difference lies in the elision: pub fn to(self, vertex: &Vertex) -> EdgeBuilder<F, [u8; 32]> {
EdgeBuilder {
to: *vertex.owner.bytes(),
.. self
}
} which is currently not possible because So, let me know whether you guys are interested in this, I may need some help doing the first RFC stuff :-) This may or may not reduce code generation for you guys. Not sure. |
Revisiting this thread. I agree with @cramertj that a
The set-once builders feel like they might belong in a separate crate, maybe? While the consuming code might not change much between the different modes, the generated docs would change radically, and any code that's handing these across a function boundary would have to change as well. |
The exactly-once semantics, session types, etc. feel out of scope for this crate, and it'd be a substantial breaking change for all existing users. I'm disinclined to ship this in |
With macros 1.1, it should be easy to implement builders like this one. This would allow the type-checker to confirm that all required (non-defaulting) fields are set exactly once. Would you consider an addition to the library implementing this feature? I'd be happy to put together a PR if there is interest.
The text was updated successfully, but these errors were encountered: