Closed dzmitry-lahoda closed 1 month ago
I could have refactor to make bigger objects to consist of smaller, or write my own builders, or custom test inputs data objects. Idea is that autogenerated builder may be useful for a while and prototype further actions, before doing all things manually(if at all ever).
Tried to quickly get things done, so as I see now builder_impl
generates single builder type.
So need some refactor to allow builder to generate many impls so each getter added iff value was set as witnessed by non generic parameter constrained specialezed.
Seems required changes are more broad than just for this single feature?
Let's discuss the design for this feature first.
First of all, I expect adding getters is not something that everyone needs. Having them generated additionally will increase the compilation time (we have crates where there are ~320 structs with builders, where compile time is important).
So getters should definitely be an opt-in
feature. Then... How do we opt-in? There should be some attribute that enables getters for specific members. There are various different ways to generate getters:
get_{member}(&self)
get_{member}_mut(&mut self)
into_{member}(self)
Then what about Copy
types? There should probably be a way to return the value by Copy
, there should be a config for this as well.
For Option
types there should also be a way to return an Option<&T>
instead of &Option<T>
.
For Deref
types there should be an attribute that configures returning &<T as Deref>::Target
Then there is probably a use case where one would like to deconstruct the builder into raw parts entirely. In this case, it would be cool if Rust had anonymous structs (https://github.com/rust-lang/rfcs/pull/2584), but alas there aren't, so this would need to be a workaround. The macro should generate some projection struct that can hold any state of the members (set or unset).
In general, this looks like a big feature that will evolve substantially. If you'd like to have some subset of this feature quickly, I'm fine to have it but granted that it's added under an unstable
cargo feature flag, and it's opt-in via attributes.
My first naive take at this would be replicating some of the API of getset
and improving it over time under an unstable
feature gate. The config should probalby be by-member. If one needs to generate setters for all members at once they can use #[builder(on(_, ...))]
to configure it at the top-level
Enabling only for specific fields sounds great, because people can pick exact places where common splits in shared partial builder happen (likely will compose nice with groups - so splits can be made around these points - as i do in manual builder anyway) with proper Option handling.
Readonly pub reference only. For mutation people use builder. Feature only for comparison and decision making, not for getting clone of value out. #[builder(get)]
.
Currently as I see I can set once, rls shows me I can set value second time, but compilation fails. So is it possible to avoid showing me setter if I have already set? Asking because for getter, I need to show getter iff I have already set. What are options here?
What about life time? My assumption is that holding reference to property will not prevent setting other fields. Any changes envisioned here?
So is it possible to avoid showing me setter if I have already set?
I don't think so. I think it's more of an issue with rust-analyzer because it doesn't take into account trait bounds when showing completions. Maybe there is a way to work around that but I don't think it's worth the effort at least now. We have a readable compile error in case the field is set the second time. This is good enough.
Especially, I want to keep the current design where there is a single impl block for the builder. This is because it makes the rustdoc
output much more cleaner and removes a lot of noise from the generated docs.
My assumption is that holding reference to property will not prevent setting other fields.
Unfortunately, Rust doesn't work that way. The getter will take &self
as an input, and return &MemberType
. It means while the &MemberType
reference returned by the getter is alive no mutations to self
can happen (especially consuming self).
current design where there is a single impl block for the builder.
i need to dig into this design. never new it is possible to do without several impls.
It means while the &MemberType reference returned by the getter is alive no mutations to self can happen (especially consuming self).
so here unsafe needed.
for sure no references could be hold if we when build is called, that is fine.
also double set same field prevented.
so after field is set, it is set. fine to continue mutate rest of builder state and hold reference to old state, so just need to use unsafe internally within well known safe patter, same as array split into 2 reference (one is mut and other not, as long as slices do not overlap).
so need to check if this https://github.com/elastio/bon/issues/97 desing works well with getters.
So I guess next step could be send proof of concept of manually modified expand of bon builder doing Getter and https://github.com/elastio/bon/issues/97 ?
so here unsafe needed.
That is currently a deal breaker for me. There is no unsafe code in bon
at this point and I don't want to start adding it right now. Especially in the context of getters. I don't think there is actually any reason to use unsafe for getters.
Better if you could provide an example case that solves some particular problem where you'd think unsafe
is fine.
what about defaults?
simplest is not to have getter for default which was not set explicitly.
any other options?
Yes, uny unset member should have no getter generated for it. We have a guarantee that default values are evaluated lazily only when the final build()
or call()
method is invoked. This way users don't need to pay the cost of evaluating the default value when they just create the builder and especially if they intend set a non-default value for the field with a setter
@Veetaha i am trying to thing up getter code to look like.
For example this simple builder expanded https://github.com/dzmitry-lahoda-forks/bon/blob/dz/2/bon-get/src/main.rs
As I see specialization of setter is done via return value.
Unfortunately I do not have return value in just field value to return.
Any ideas how to make it nice?
I tinkering next:
So is it possible at all to keep single impl or go right away with many?
Mat be pub trait IsSet {}
trait? Is goal to avoid many impls in expanded code?
--> bon-get/src/main.rs:187:16
|
187 | let c= aaa.get_c();
| ^^^^^ the trait `IsSet<Option<Vec<u8>>>` is not implemented for `Unset<Optional>`
|
with
#[allow(clippy::inline_always, clippy::impl_trait_in_params)]
#[inline(always)]
pub fn get_c(&self) -> Option<&Vec<u8>>
where
__C: ::bon::private::IsSet<Option<Vec<u8>>>,
{
self.__private_members.2.is_set().as_ref()
}
@Veetaha what about String vs str? I feel would not change String to str? So will return &String.
As I see specialization of setter is done via return value.
What return value do you mean? Could you give more context or examples?
So is it possible at all to keep single impl or go right away with many?
I'd like to keep a single impl block. We have Unset<Required/Optional>/Set<T>
types for which we can define new traits to get their state in a generic way.
For example an IsSet
trait that is only implemented for the Set<T>
and has a method to get the inner &T
value.
what about String vs str? I feel would not change String to str? So will return &String.
I wonder what getset
has for this. Is there a way to configure something like get(deref)
in getset
?
From remaining questions I have only deref, as I see getset does not do it https://github.com/jbaublitz/getset/issues/35 . So it can be added later if needed. Things like copy/deref can be added later.
please reopen when feature would have more broad usage.
real world usage
proptest
allows to chain into hierarchy generated properties viaprop_flat_map
(with optimized shrinking retained).so each
prop_flat_map
in chain depends on data to be generated from upper layer, uses that data to constraint its out put.prop_flat_map(|prev| prev, prev+1 ).prop_flat_map(|(prev_0, prev_1|, ..., 0..prev_1).
so final test function gets
do_test(prev_0, prev_1, prev_2)
.so I thin of using builder on each stage and path only partial builder, instead of many parameters. but need access to already set fields. in the end I do call build and produce final data struct.
example without builder:
https://altsysrq.github.io/proptest-book/proptest/tutorial/higher-order.html
A note for the community from the maintainers
Please vote on this issue by adding a π reaction to help the maintainers with prioritizing it. You may add a comment describing your real use case related to this issue for us to better understand the problem domain.