bon icon indicating copy to clipboard operation
bon copied to clipboard

Generic type for an incomplete builder?

Open rollo-b2c2 opened this issue 1 year ago • 4 comments

Appreciate this might be quite difficult to do:

If you want to pass around a builder you have to specify the state of the builder:

#[bon::builder]
pub struct T {
    a: usize,
    b: usize
}

fn a() -> TBuilder<(bon::private::Set<usize>, bon::private::Set<usize>)> {
    T::builder()
        .a(1)
        .b(2)
}

let var = a1();
var.build();

let var = a2().b(2);
var.build();

For complete structs it would make sense to type alias completeness to

type TBuilderValid = (bon::private::Set<usize>, bon::private::Set<usize>);

However what would be really useful would be some generic way of passing incomplete builders around.

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.

rollo-b2c2 avatar Jul 30 '24 12:07 rollo-b2c2

However what would be really useful would be some generic way of passing incomplete builders around.

Indeed. right now the builder type signature is unstable. You can see that it references types from a #[doc(hidden}] internal module bon::private.

https://github.com/elastio/bon/blob/3a67fdf40b731fad2bbc0da21b3a1afd943559ea/bon/src/lib.rs#L5-L7

Unfortunately, I don't see any good convenient syntax to expose the type states of the builder. As you can see today the builder's __State generic parameter expects a tuple that mentions the states of every member in order. This means the signature is positional. There is no clear association between tuple items and the member names that they correspond to syntactically.

Also, when specifying the type state for the builder one needs to always specify states for all members. This is very-very inconvenient.

Another way to do this could be abusing the dyn trait Rust feature with the syntax TBuilder<dyn BuilderState<Field1 = Set, Field2 = Unset, Field3 = Unset, Field4 = Optional>> to specify the states with some name to it, but it requires you to specify values for all associated trait types.

I think this may be simplified if default associated types in traits are stable https://github.com/rust-lang/rust/issues/29661.


Another thing. Do you have an immediate example real use case for this feature?

Some keywords for this issue: partial builder type, builder type state syntax, stable builder type signature

Veetaha avatar Jul 30 '24 12:07 Veetaha

For complete structs it would make sense to type alias completeness to

I'm not sure how this could be useful in general. One use case that I can think of is to prepare the parameters for a function without actually calling it and then storing the these parameters in the "complete" builder somewhere to lazily invoke the .call() method somewhere later. Is this your thinking as well?

If so, as a compromise solution bon's codegen may be extended to give a type alias for such a "complete" builder.

Note that the builder captures all the lifetimes and generic type parameters from the function and impl block (if it is under an impl block). These generic params need to be part of the "complete" builder type alias. This is already the case today for the "initial" builder state today though. That type name is kinda public, although not advertised in the docs.

Veetaha avatar Jul 30 '24 12:07 Veetaha

Some more thoughts on this issue:

There isn't just one terminal state for the builder where you can call the finishing functions (call() or build()).

For example:

#[builder]
struct Foo {
    a: Option<String>,
    b: Option<String>,
}

// `FooBuilder<(Optional<Option<String>>, Optional<String>)>`
Foo::builder().build();

// `FooBuilder<(Set<Option<String>>, Optional<String>)>`
Foo::builder().a("a").build();

// `FooBuilder<(Optional<String>, Set<Option<String>>)>`
Foo::builder().b("b").build();

// `FooBuilder<(Set<Option<String>>, Set<Option<String>>)>`
Foo::builder().a("a").b("b").build();

In the generated code the terminal states are covered by the #[doc(hidden)] trait IntoSet which expresses the possibility of converting Optional<Option<T> into Set<Option<T>.

Here is how the generated impl block for the build() method looks like in this case:


    impl<__State: __FooBuilderState> FooBuilder<__State>
    where
        __State::A: bon::private::IntoSet<Option<String>>,
        __State::B: bon::private::IntoSet<Option<String>>,
    {
        #[doc = r" Finishes building and performs the requested action."]
        fn build(self) -> Foo {
            Foo {
                a: ::bon::private::IntoSet::into_set(self.__private_impl.a).into_inner(),
                b: ::bon::private::IntoSet::into_set(self.__private_impl.b).into_inner(),
            }
        }
    }

This means there is no single "complete" builder type state. So it's not possible to express it with just a single type alias. It'll probably need to be a trait:

trait TBuilderComplete {
     fn build(self) -> T;
}

And that impl block higher can be turned into this trait impl instead. Then we can make the trait name exposed from the generated code.

Unfortunatelly this trait isn't object-safe, unless maybe there could be a method that takes Box<Self> like fn boxed_build(self: Box<Self>) -> T. This way the caller would be able to store the "complete" builder in a Box<dyn TBuilderComplete>, but... that requires an allocation. This however won't be a problem once impl Trait in type aliases becomes stable (https://github.com/rust-lang/rust/issues/63063).

Veetaha avatar Jul 31 '24 10:07 Veetaha

I've implemented a way to denote the incomplete builder type (with some members set).

See #145 for details. It should close this issue once ready

Veetaha avatar Sep 22 '24 01:09 Veetaha