Reinterpret generic type
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)]
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 Thanks! Your snippet is very inspring.
However, I met two further difficulties if using this approach:
- I'm also deriving
argh::FromArgsargument parser on the same struct (and perhapspyo3::FromPyObjandwasm_bindgenin the future for python and js binding), so splitting it into a generic-lessQuerystruct is not an option. - The experience of
extrais 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)].