Idea: explicit type parameters for derivation

Motivation

Idea

We could add explicit type parameters in the derive attribute, indicating which ones ought to be used as constraints in the expanded derivation. These would be equivalent:

// The derivations require `T: Copy` and `T: Clone` respectively
#[derive(Copy, Clone)]
struct Y<T>(&'static fn(T));

// explicitly constrain `T` the same way
#[derive<T>(Copy, Clone)]
struct Z<T>(&'static fn(T));

But with an explicit list, you could choose partial constraints, or even no constraints at all.

// requires `T: Copy` and `T: Clone`, but `U` is not constrained
#[derive<T>(Copy, Clone)]
struct Partial<T, U>(T, &'static fn(U));

// the derivations are unconstrained
#[derive<>(Copy, Clone)]
struct Free<T>(&'static fn(U));

You can also use different constraints for different derives:

#[derive(PartialEq, Eq)] // implicitly constrained on `T` (status quo)
#[derive<T>(PartialOrd, Ord)] // explicitly constrained on `T`
#[derive<>(Clone)] // unconstrained
struct Foo<T>(Arc<T>);

If you need more complicated constraints, thereā€™s still a normal impl blockā€¦

Alternatives

After I started writing this, I found @centrilā€™s RFC 2353:

In other words, we can probably ignore this, since that RFC is fully fleshed out. But hey, it's Friday, and I think there's no harm in sharing another idea.

3 Likes

Interesting; I had not considered this surface syntax as a means of achieving the intended effect. Using #[derive<T>(...)] might be more ergonomic overall, which is nice because Iā€™m not entirely satisfied with #[derive_no_bound(..)].

However, I believe the current attribute grammar does not support #[derive<T>(...)] so we would presumably need to extend it to accept this. cc @petrochenkov

Another aspect to consider is how custom derive macros are going to become aware of the <T> here. Currently, I believe they are not told about the #[derive(...)] bit.

3 Likes

Maybe there could be a new #[proc_macro_derive_bikeshed] to opt in to taking another parameter.

We can probably reuse the same attribute proc_macro_derive and detect whether there's an additional parameter in the function instead.

Another approach, which might be more useful overall, is to just pass the #[derive(...)] stuff to the derive macro itself. That would also allow custom derive macros to do things like "consider the other derived traits" like #[derive(Copy, Clone)] does for optimization purposes.

1 Like

As a natural extension to the #[derive<T>(Copy, Clone, ...)] syntax, it seems to me that you could also allow:

#[derive<T: Send>(Copy, Clone, ...)]

This would simply override all the bounds on T to be where T: Send.

2 Likes

Could we upgrade derive to a standalone language construct?

impl derive Copy, Clone for Foo<T> where T: Send;
6 Likes

I feel that instead of extending the syntax of attributes to accept quasi type arguments we could start accepting attribute annotations on type arguments themselves.

The syntax seems to suggest things like these will be possible. Not sure if these are good things.

impl<T> derive Clone for Box<MyType<T>>;

impl derive Clone for MyWrapper<OnlyThisVerySpecialType>;
impl derive Clone for MyWrapper<AnotherVerySpecialType>;

Also, this syntax canā€™t work with proc_macro_derive. Proc macros donā€™t have type information.

impl<X: Serialize + Send> derive Serialize for MyType<X> {}
// ok how `serde` is able to read about fields of `MyType<X>`?
//
// how's the user going to tell serde about `#[serde(rename = "...")]`
// for some fields?

This was discussed in the RFC where you raised the point as well. See https://github.com/rust-lang/rfcs/pull/2353#issuecomment-385244811 for my thoughts on this. In brief, a mechanism like impl derive would be non-macro based invasive change to the compiler because you don't have access to the tokens necessary to tell what the fields of Foo are. Given that it would be non-macro based, I don't see how it can interact with custom derive as alluded to by @kennytm.

We already support attributes on type parameters; e.g. impl<#[may_dangle] T> ... is valid. This is what the cited RFC uses:

#[derive(Clone)]
struct MyArc<#[derive_no_bound] T>(Arc<T>);

I do think that:

#[derive<T>(Clone)]
struct MyArc<T>(Arc<T>);

is both more readable and ergonomic to write.

If you had some other idea, please elaborate. For actual attributes on type arguments, this would be syntactically supported by:

5 Likes

:+1:

I keep feeling like these more complex attribute things are wanting to be real syntax instead of just a macro. By the time there are expressions or trait bounds or etc in a macro, it seems like all the things that are needed to have that go well (name lookup, autocomplete, etc) aren't worth it.

2 Likes

Oh, no, please, letā€™s not do that. Derive's beauty is exactly that itā€™s a macro and as such, custom derives are mostly uniform (at the level of basic mechanism) with the other kinds of macros, and they can also be expanded without additional support from the compiler (all the more, without needing to invent non-parseable dummy syntax in the expanded output).

3 Likes

Simply isnā€™t this possible?

derive Copy, Clone for Foo<T> where T: Send;

They're already invoked differently from other proc macros (#[derive(foo)] vs foo!{}) and defined differently from other proc macros (proc_macro_derive vs proc_macro), so I don't see a fundamental problem with potentially changing up either of those.

For example, I could imagine a "by example" version of derive that works for things that are field-by-field, the way so many of the basic derives are, even if one needs full proc derive macros for things like generating stuff from SQL schemas.

1 Like

@centril, whatā€™s the next step? Are you going to adopt this syntax in your RFC, or should I open a competing RFC with this change?

The other ideas in here of a first-class (non-attribute) derive would surely deserve their own new RFCs, if those folks would like to pursue that.

I was hoping to hear from @petrochenkov re. the feasibility of supporting #[derive<T>(tts)] in the attribute grammar.

Depending on @petrochenkov's, sure. It would be nice to collaborate on the adjustments needed to the RFC.

However, my existing RFC, changes to it, or a completely new RFC are unlikely to be roadmap goals at the moment for the language team so please don't expect swift action here from us. :slight_smile:

Well... given @scottmcm's noted views here, I think perhaps it is a good idea to discuss things some more so that there isn't a core semantic disagreement wrt. macros vs. a special language construct. That is, It would be good if we weren't what appears to be miles apart.

This things already happen and allow users to encode what they need ergonomically. Notably, serde_derive allows you to bake bounds into strings (suboptimal, and the language now allows it to be improved). Crates like snafu and failure also allow expression like things. proptest_derive allows you to bake expressions directly into the deriving logic.

Since it already happens, the better question seems to me whether we can/should provide a more common interface that gives a uniform UX across the ecosystem. Making a minor change in terms of compiler complexity by supporting #[derive<...>(...)] would do that and would provide a lot of benefits for little complexity.

How is this going to work practically? "wanting to be real syntax" is perhaps a nice sentiment but it needs to be backed by some mechanism. The way #[derive(..)] work right now is completely syntax directed based on the "text" of the thing it is applied to. If you have impl derive Copy, Clone for Foo<T> where T: Send; then:

  • how will you deal with phase separation issues wrt. not having access to the definition of Foo yet?
  • how will you support custom traits?

Also, whatever "real syntax" alternative you come up with, it will presumably be complicated for the compiler to implement because its a whole other system to manage concurrently with the macro one. It will also be a whole new system users must learn... What justifies this added complexity as compared to tweaking #[derive(...)]? Moreover, why is this "real syntax" more ergonomic than tweaking #[derive(...)? It seems to me that something like impl derive Copy, Clone for Foo<T> where T: Send; is both more verbose and is also not located with the type definition and so a reader does not get to enjoy the same syntax-directed understanding of the implementation as the compiler would.

That is a good idea. We have 3 basic forms of macros we are interested in: bang!(...), #[attribute(...)], and #[derive(...)]. All 3 of those can be written as procedural macros but only bang!(...) macros can be written as macro_rules! ones. I see no fundamental reason why macro_rules! could not also support #[derive(...)] and #[attribute(...)] macros.

I think the question of whether we should support custom derive macros based on declarative macros is still missing that both procedural and declarative macros are macros. Declarative ones will, to my knowledge, still have the same phase separation issues as procedural ones.

1 Like

Iā€™d rather want https://rust-lang.zulipchat.com/#narrow/stream/144729-wg-traits/topic/coinductive.20all.20the.20way to work out.

1 Like

But letā€™s assume for a moment that it does not. Putting aside the desirability of such a change, how feasible do you think it is technically to support e.g.

Attr = "[" path:Path generics:AttrGenerics? input:AttrInput "]" ;

AttrGenerics = "<" TOKEN_STREAM  ">" ;

// As defined today; no change:
AttrInput =
  | {}
  | "(" TOKEN_STREAM ")"
  | "[" TOKEN_STREAM "]"
  | "{" TOKEN_STREAM "}"
  | "=" LITERAL
  ;

?

I definitely think that [ path < args > = value ] is undesirable, so the ā€œgeneric argumentsā€ should only be available on bracketed forms.