Skip to content
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

Generic type for an incomplete builder? #21

Open
rollo-b2c2 opened this issue Jul 30, 2024 · 4 comments · May be fixed by #145
Open

Generic type for an incomplete builder? #21

rollo-b2c2 opened this issue Jul 30, 2024 · 4 comments · May be fixed by #145
Labels
feature request A new feature is requested

Comments

@rollo-b2c2
Copy link

rollo-b2c2 commented Jul 30, 2024

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.

@Veetaha Veetaha added enhancement New feature or request design needed The feature requires more design effort labels Jul 30, 2024
@Veetaha
Copy link
Contributor

Veetaha commented Jul 30, 2024

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.

bon/bon/src/lib.rs

Lines 5 to 7 in 3a67fdf

/// Symbols used by macros. They are not stable and are considered an implementation detail.
#[doc(hidden)]
pub mod private;

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 rust-lang/rust#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
Copy link
Contributor

Veetaha commented Jul 30, 2024

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 Veetaha added feature request A new feature is requested and removed enhancement New feature or request labels Jul 30, 2024
@Veetaha
Copy link
Contributor

Veetaha commented Jul 31, 2024

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 (rust-lang/rust#63063).

@Veetaha
Copy link
Contributor

Veetaha commented Sep 22, 2024

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 Veetaha removed the design needed The feature requires more design effort label Sep 22, 2024
@Veetaha Veetaha linked a pull request Sep 22, 2024 that will close this issue
24 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request A new feature is requested
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants