builder-pattern icon indicating copy to clipboard operation
builder-pattern copied to clipboard

Default type params

Open cormacrelf opened this issue 2 years ago • 7 comments

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:

  1. Make the fn new() return a builder with the default types substituted in their spots.
  2. 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 using fn update<FUpdate_>(update: FUpdate_) and doing lots of substitutions on the bounds / generic params.
  3. 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.
  4. Lots of minor improvements, much of it relating to commas and macro edge cases.
  5. Finally, an extra feature for fields like phantom: PhantomData<T> where T was specified with a default type T = f64 + a separate #[infer(T)] field. It needed special handling because if the macro writes a PhantomData in the fn new(), then it has to write another one with the inferred T_ (which means a different PhantomData<T_> type needs to be in the phantom field. So I introduced "late binding" of defaults, where they are only populated in the fn build() function, but the types are carried along the way. This required simple compile-time reflection in the form of the refl crate, which I pulled a few select functions from, in order to prove that if you still had a Some(Setter::LateBoundDefault) in the field, that basically T = T_. It worked pretty well!
  6. 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.

cormacrelf avatar May 07 '23 15:05 cormacrelf

Thanks for your kind contribution! I'll look at it.

SeokminHong avatar May 09 '23 02:05 SeokminHong

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 avatar May 09 '23 07:05 cormacrelf

@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.

SeokminHong avatar May 09 '23 16:05 SeokminHong

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 avatar May 09 '23 22:05 cormacrelf

@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.

SeokminHong avatar May 10 '23 01:05 SeokminHong

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:

image

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)]

cormacrelf avatar May 10 '23 10:05 cormacrelf

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!

SeokminHong avatar May 10 '23 11:05 SeokminHong