-
Notifications
You must be signed in to change notification settings - Fork 3
Default type params #27
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
base: main
Are you sure you want to change the base?
Default type params #27
Conversation
Thanks for your kind contribution! I'll look at it. |
One todo I forgot to mention is reproducing what I did for normal setters in the lazy and async cases. I think it should be easy. At the very least they could produce a |
|
||
tokens.extend(quote! { | ||
impl <#impl_tokens> #ident <#(#lifetimes,)* #ty_tokens> #where_clause { | ||
impl <#impl_tokens> #ident <#(#lifetimes,)* #ty_tokens> #where_tokens { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
struct X<A, B = f64> {
a: A,
b: B
}
It will generate the following code for the above struct:
impl<A> X<A, f64> {
fn new<'fn_lifetime>() -> XBuilder<'fn_lifetime, A, f64, (), (), (), (), ()> {}
}
X::<i32>::new(); // ok
X::new().a(3); // ok
X::new().a(3).b(5); // expected: f64, given: i32
X::<i32, i32>::new(); // No associated function `new`
For the as-is way, it will generate the following code:
impl<A, B> X<A, B> {
fn new<'fn_lifetime>() -> XBuilder<'fn_lifetime, A, B, (), (), (), (), ()> {}
}
X::<i32>::new(); // ok
X::new().a(3); // Cannot infer B
X::new().a(3).b(5); // ok
X::<i32, i32>::new(); // ok
I think the as-is case can handle more cases. Is this change required for implementing the infer
attribute?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit like the difference between HashMap::new
and HashMap::default
, which becomes apparent when you use an alternative hash algorithm (e.g. fnv::FnvHashMap
).
impl<K, V> HashMap<K, V, RandomState> { fn new() -> Self { ... } }
// whereas
impl<K, V, S> Default for HashMap<K, V, S> where S: Default { ... }
To use any other hasher than the default DOS-protected RandomState one, you can't call new
, you must use e.g. fnv::FnvHashMap::default()
. The standard library chose to write new
that way because otherwise it's impossible to infer S
in 99% of people's code: the S parameter is stored in a PhantomData, not passed in, and hence not inferable except when your hash map is about to be returned from a function (with -> HashMap<K, V>
and hence a default S param) or passed to a function / placed in a structure.
If we translate that into builder-pattern's terms, the use case is when the b
field has a #[default(...)]
attribute and is therefore optional. In those cases you need the type checker to choose something when you do not provide b.
I think in general you need two ways of creating a builder, one with the default types substituted (like HashMap::new) and one with them as generic params (like HashMap::default). In my view the most user-friendly way to do it would be to do the same thing as HashMap, and have the Default impl be the generic one. This is also how all the A = Global
allocator params on all of the standard library's collection types are done. Vec::new()
gives you a Vec<T, Global>
, and so on.
It seems that this PR's behaviour would be a breaking change to builder-pattern, if it's the only option. So maybe we could add a struct-level attribute to accept & substitute the default type params in the new
function. I still think it makes more sense to follow the standard library's behaviour and put out a version bump, but at least there are options. In either case implementing Default
is a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ability to choose which ones are substituted in the new
function would be quite nice. Here's a comparison of two methods of choosing.
Method | Breaking change? | Behaviour without customization | what the attribute does |
---|---|---|---|
#[infer_new(B)] struct X... | yes | like std's HashMap, Vec, etc | allows inference of B in new , prevents #[default(...)] b: B from working |
#[new_default(B)] struct X... | no | like existing builder-pattern, not like std | substitutes B=f64 in new , allows #[default(...)] b: B to work |
#[infer_new]
also matches up with #[infer]
, learn it once and understand both. Whereas there is no way to name the #[new_default]
attribute in a way that doesn't require reading docs eight times, because there are already #[default]
attributes that do completely different stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand what you meant, and I think substituting way is more suit for builder-pattern. In my thought, it doesn't seem idiomatic to me that cannot call the new
function for overriding default types.
@cormacrelf Using |
Great question, I haven't tried it with everything late bound AND the refl tricks. It broke examples/default-fn.re without the refl cast IIRC. |
@cormacrelf Now I agree your suggestion works like If you agree, I'll merge this PR to the new |
I think it would be possible to implement it as your table. When users set default types for generic types for a struct, I think it also implies that the types are inferred automatically. I'll try it this weekend! |
This adds quite a lot of functionality. The intended use case was a struct like this. It obviously has a problem because if you do not specify
update
orspecialized_initial
, then the compiler doesn't know what type they should be. This is because the macro was not utilising the default type params (which provide fn-pointers, which are enough to compile with since they're never executed because they're None).The solution required a bunch of changes:
fn new()
return a builder with the default types substituted in their spots.update
, the FUpdate closure type can be different from the default. Hence you need to get the setter method to infer the FUpdate, and replace it in the return type. This is done usingfn update<FUpdate_>(update: FUpdate_)
and doing lots of substitutions on the bounds / generic params.#[infer(T1, T2)]
param. For this example you apply it to the update and specialized_initial fields, with#[infer(FUpdate)]
and#[infer(FInitial)]
respectively.phantom: PhantomData<T>
where T was specified with a default typeT = f64
+ a separate#[infer(T)]
field. It needed special handling because if the macro writes a PhantomData in thefn new()
, then it has to write another one with the inferred T_ (which means a differentPhantomData<T_>
type needs to be in the phantom field. So I introduced "late binding" of defaults, where they are only populated in thefn build()
function, but the types are carried along the way. This required simple compile-time reflection in the form of therefl
crate, which I pulled a few select functions from, in order to prove that if you still had aSome(Setter::LateBoundDefault)
in the field, that basicallyT = T_
. It worked pretty well!#[hidden]
fields. It also needed some way of doing it while also generating a setter method, so I made#[late_bound_default]
, but it might need a better name.Altogether it needs some more docs, and I want to split out some of the example code, but the rest of it should be reviewable.