bon icon indicating copy to clipboard operation
bon copied to clipboard

Reinterpret generic type

Open xbwwj opened this issue 5 months ago • 2 comments

Description

Allow reinterpret the generic type in builder.

For simple skip case:

#[derive(Builder)]
struct Foo<T> {
    #[builder(skip)]
    foo: Option<T>
}

impl<T1, S> FooBuilder<T1, S> {
    // change generic T1 to T2
    // here `foo` is name of the field
    fn reinterpret_foo<T2>(self) -> FooBuilder<T2, S> {
        todo!()
    }
}

For IsUnset case: reinterpet is only allowed for unset fields.

#[derive(Builder)]
struct Bar<T> {
    bar: T
}

impl<T1, S> BarBuilder<T1, S>
where
    S::Value: IsUnset
{
    fn reintepret_bar<T2>(self) -> BarBuilder<T2, S> {
        todo!()
    }
}

Implementation

The implementation is very trivial. For example, for skip case:

impl<T1, S> FooBuilder<T1, S>
{
    #[allow(deprecated)] // <- generating deprecated warnings here
    fn reinterpret_foo<T2>(self) -> FooBuilder<T2, S>
where {
        let Self {
            __unsafe_private_named,
            ..
        } = self;
        FooBuilder {
            __unsafe_private_phantom: PhantomData,
            __unsafe_private_named,
        }
    }
}

But it requires destructing the builder and fires a deprecated warning. So I think it should better be implemented in bon itself rather than user side.

A use case

This method is useful for default generic implementation and user customizable trait impl inside builder struct.

Imagine the case that we have a raw API which takes a string input and returns many fields:

fn fake_query_user_info_raw(name: &str) -> serde_json::Value {
    json!({
        "name": name,
        "age": 30,
        "phone": "123456789",
        "department": "Accounting",
        // many more fields in real world
    })
}

We want to provide a wrapper that deserialize part of the fields in advance, but still allow the user to customize deserialization for more fields, using the #[serde(flatten] feature.

#[derive(Deserialize, Debug)]
struct Message<Extra = ()> {
    // provided by our wrapper
    name: String,
    age: u32,

    // user customizable
    #[serde(flatten)]
    extra: Extra,
}

We want to provide wrapper API like below.

// The simple API call, no need to type generic
let simple = query_user_info().name("Alice").fetch();
dbg!(simple);

// Advanced users can provide custom deserialize
let advanced = query_user_info().name("Alice").extra::<PhoneExtra>().fetch();
dbg!(advanced);

#[derive(Deserialize, Debug)]
struct PhoneExtra {
    phone: String,
}

To craft such an API, we define the parameters with bon.

/// Arguments to the API.
#[derive(Builder)]
pub struct Args<Extra = ()> {
    #[builder(into)]
    name: String,

    #[builder(skip)]
    _extra: PhantomData<Extra>,
}

and provide custom builder methods:

impl<Extra, S> ArgsBuilder<Extra, S> {
    fn extra<NewExtra>(self) -> ArgsBuilder<NewExtra, S>
where {
        self.reinterpret_extra::<NewExtra>()
    }
}

then wrappers:

fn query_user_info() -> ArgsBuilder {
    Args::builder()
}

impl<Extra, S> ArgsBuilder<Extra, S>
where
    S: args_builder::IsComplete,
{
    fn fetch(self) -> Message<Extra>
    where
        Extra: DeserializeOwned,
    {
        let args = self.build();
        let raw = fake_query_user_info_raw(&args.name);
        serde_json::from_value(raw).unwrap()
    }
}

Why prefer such API?

As Rust does not allow default generic in function, users have to bind the generic in front,

let simple = query_user_info::<()>.name("Alice").fetch();

This syntax poses more cognitive and typing burden for lightweight users, compared to the former one.

More to discussion

  • the syntax: automatically generated for generic field, or requires explicit #[builder(reinterpret)]
  • reinterpret bound: #[builder(reinterpret(bound = T)]

xbwwj avatar Aug 11 '25 12:08 xbwwj

Hi! I think the API that you want (i.e. extra::<T>().fetch() or just fetch() with a default generic param) can be achieved using stable bon's features this way:

use std::marker::PhantomData;

#[derive(Debug)]
struct Response<T> {
    common_field: usize,
    extra: T,
}

#[derive(Debug, serde::Deserialize)]
struct PhoneExtra {
    phone: String,
}

#[derive(serde::Deserialize)]
struct EmptyExtra {}

// Make the QueryBuilder struct itself public, but the `build()` method private.
// Callers can finish building only with the `fetch()` method wrapper instead
#[derive(bon::Builder)]
#[builder(builder_type(vis = "pub"), finish_fn(vis = ""))]
struct Query {
    // Define only the fields that don't use the generic parameter
    #[builder(into)]
    name: String,
}

impl<S: query_builder::IsComplete> QueryBuilder<S> {
    // Default case when no extra is needed.
    pub fn fetch(self) -> Response<()> {
        self.extra::<EmptyExtra>().fetch()
    }

    pub fn extra<T>(self) -> QueryWithExtra<T> {
        QueryWithExtra {
            base: self.build(),
            extra: PhantomData,
        }
    }
}

pub struct QueryWithExtra<T> {
    base: Query,
    extra: PhantomData<T>,
}

impl<T: serde::de::DeserializeOwned> QueryWithExtra<T> {
    pub fn fetch(self) -> Response<T> {
        // Here would be the real code that does the fetch and parses the response.
        // It has all the necessary parameters available via `self` here
        Response {
            common_field: self.base.name.len(),
            extra: serde_json::from_str("...").unwrap(),
        }
    }
}

fn query_user_info() -> QueryBuilder {
    Query::builder()
}

// The API from the issue works:
fn main() {
    // The simple API call, no need to type generic
    let simple = query_user_info().name("Alice").fetch();
    dbg!(simple);

    // Advanced users can provide custom deserialize
    let advanced = query_user_info()
        .name("Alice")
        .extra::<PhoneExtra>()
        .fetch();
    dbg!(advanced);
}

However, I would simplify such api and instead have a pair of methods fetch() and fetch_with_etxtra<T>() to avoid having an intermediate QueryWithExtra<T> struct.

Otherwise, the problem of reinterpreting generic parameters is quite a complex problem. Generic parameters can be used in where clause bounds, which makes this so hard. I doubt we should do something that complex in bon, unless there is a reasonable solution like the one higher that avoids that complexity. So do you think the snippet I shown above solves this problem for you?

Veetaha avatar Aug 11 '25 17:08 Veetaha

@Veetaha Thanks! Your snippet is very inspring.

However, I met two further difficulties if using this approach:

  1. I'm also deriving argh::FromArgs argument parser on the same struct (and perhaps pyo3::FromPyObj and wasm_bindgen in the future for python and js binding), so splitting it into a generic-less Query struct is not an option.
  2. The experience of extra is downgraded as it can only be called in the last.

For now I'll keep with the destructing approach in my project, and suppress the warnings using #[allow(deprecated)].

xbwwj avatar Aug 11 '25 19:08 xbwwj