Default type params
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 or specialized_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).
#[derive(Builder)]
pub struct ClosureFold<K, V, R, FAdd, FRemove,
FUpdate = for<'a> fn(R, &'a K, &'a V, &'a V) -> R,
FInitial = for<'a> fn(R, &::im_rc::OrdMap<K, V>) -> R
> where
FAdd: for<'a> FnMut(R, &'a K, &'a V) -> R + 'static,
FRemove: for<'a> FnMut(R, &'a K, &'a V) -> R + 'static,
FUpdate: for<'a> FnMut(R, &'a K, &'a V, &'a V) -> R + 'static,
FInitial: for<'a> FnMut(R, &::im_rc::OrdMap<K, V>) -> R + 'static,
{
pub add: FAdd,
pub remove: FRemove,
#[default(None)]
pub update: Option<FUpdate>,
#[default(None)]
pub specialized_initial: Option<FInitial>,
#[default(false)]
pub revert_to_init_when_empty: bool,
#[default(PhantomData)]
#[hidden]
pub phantom: PhantomData<(K, V, R)>,
}
The solution required a bunch of changes:
- Make the
fn new()return a builder with the default types substituted in their spots. - When you provide
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. - However, because you don't know what shape the field type will be, you need to tell the macro which params to do this with. Hence there's a new
#[infer(T1, T2)]param. For this example you apply it to the update and specialized_initial fields, with#[infer(FUpdate)]and#[infer(FInitial)]respectively. - Lots of minor improvements, much of it relating to commas and macro edge cases.
- Finally, an extra feature for fields like
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 thereflcrate, 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! - Late binding is applied by default to
#[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.
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 compile_error!("infer not supported with async setter"), if implementing that proves complicated.
@cormacrelf Using refl seems amazing! I wonder why you distinguish late_bound_default and default? It seems that using just late_bound_default only can handle every case.
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 I finally understand the meaning of this PR 😂. It was quite difficult to understand for me at first because I didn't expect this detailed usage.
Now I agree your suggestion works like Default::default methods for implicit type inference. However, it adds some complexities to declaring attributes. So, what about using infer and late_bound_default by default for each field?
If you agree, I'll merge this PR to the new dev branch and work on it.
If #[infer] were the default, you would need to disable it (e.g. #[no_infer(T)]) on every other field. This is what happens when you simulate your proposed behaviour:
It fails because the B_ param inserted into fn field_a<B_>(field_a: A) -> XBuilder<A, B_, ...> does not appear in the field type. It's not inferable. Most fields will not be able to infer anything about type params that don't appear in their setter function args, so most fields will end up having a completely free type parameter that the compiler has no way of choosing something for.
The best you could do i think would be trying to find the type param ident inside the field type, and automatically infer that param if it appears. For this example, field_b: B obviously has a B in it, so it would automatically get #[infer(B)]. You'd be looking for B at the beginning of a syn::Path, or something. The token-stream type substitution is a bit dumber though.
With struct X<A, B=f64, C=f64>:
| field type | auto-infer? |
|---|---|
B |
#[infer(B)] |
(A, B) |
#[infer(B)], no need to infer A as that's done throughout the builder calls |
(B, C) |
#[infer(B, C)] |
Vec<(A, C)> |
#[infer(C)] |
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!