Here's something to ponder on between your RustFest sessions.
Summary
Add ability to pass generic parameters of the impl to the derive macros,
greatly increasing the flexibility of the derive
attribute.
Motivation
Derive macros are a very convenient way to generating trait impls based on
the definition item of a type. However, the ability to use #[derive(Trait)]
is denied when the impl must have generic parameters that need to be defined
and bound in a more customized way than what the derive macro could generate
automatically based on the definition item of the Self
type.
Consider The Most Annoying Problem of #[derive(Clone)]
:
#[derive(Clone)]
pub struct WaitingForGodot<T> {
// ...
_phantom_godot: PhantomData<T>
}
The use of derive
here is often a convenient pitfall that generates this impl:
impl<T: Clone> Clone for WaitingForGodot<T> {
// ^---- Oops, did not really need this bound
// ...
}
This can be easily solved by customizing the impl parameter:
#[derive(<T> Clone)]
pub struct WaitingForGodot<T> {
// ...
_phantom_godot: PhantomData<T>
}
More traits could be made conveniently derivable with custom generics than is feasible now:
use derive_unpin::Unpin;
#[derive(<St: Unpin, F> Unpin)]
pub struct MyFold<St, F> {
#[unsafe_pinned]
stream: St,
#[unsafe_unpinned]
op: F,
}
In tandem with more elaborate helper attributes, it could be even more powerful:
// A not-yet-written library providing the derive macro
use async_state_machine::Future;
use futures::future::{TryFuture, IntoFuture, MapOk};
#[derive(
<
Fut1: TryFuture,
Fut2: TryFuture<Error = Fut1::Error>,
F: FnOnce(<Fut1 as TryFuture>::Ok) -> Fut2,
> Future
)]
enum AndThen<Fut1, Fut2, F> {
First(MapOk<Fut1, F>),
#[after(First)]
#[future(output)]
Then(IntoFuture<Fut2>),
}
Guide-level explanation
The trait name in a derive
attribute can be adorned with generic parameters
that specify the generics of the generated impl
item:
#[derive(<T: Bound1, U: Bound2> Frob<T>)]
struct Foo<U> {
// ...
}
The derive macro for Frob
is expected to generate an implementation item
with these generic parameters:
impl<T: Bound1, U: Bound2> Frob<T> for Foo<U> {
// ...
}
Reference-level explanation
The syntax of an item in the derive
attribute is extended to a subset of the
language that can occur in a trait implementation item between the keywords
impl
and for
:
DeriveItem :
Generics? TypePath
The procedural macro can optionally support generic parameters to derive
by
defining an entry point annotated with the proc_macro_derive_with_generics
attribute:
extern crate proc_macro;
use proc_macro::TokenStream;
#[proc_macro_derive_with_generics(Frob)]
pub fn derive_frob_with_generics(
generics: TokenStream,
trait_args: Option<TokenStream>,
item: TokenStream,
) -> TokenStream {
// ...
}
Invoked in the example above, the function will receive the token stream of
<T: Bound1, U: Bound2>
as the first argument, a Some
value with the token
stream of <T>
as the second argument, and the token stream with the
struct Foo
item as the third.
If the compiler does not find a matching proc_macro_derive_with_generics
symbol in the procedural macro crate that it has resolved for a derive
item
that features generics, an error is reported stating that the macro does not
support generics. A plain old derive
item can be processed with
a function annotated as proc_macro_derive_with_generics
if no function
is annotated as proc_macro_derive
for the same trait, otherwise the other
function gets called.
Drawbacks
This extension complicates the syntax of the derive
attribute.
Rationale and alternatives
Extending derive
this way, we can solve its current shortcomings and
open it to more uses and experimentation.
Everything proposed here is also possible to implement with custom attribute
macros. But this would unnecessarily multiply mechanisms for generating a trait
implementation for a type. Plugging into a well-defined syntax of the
derive
attribute would make the macro more convenient for the users
and may be more friendly to automatic analysis than freeform attribute macros.
Prior art
The author is not aware of metaprogramming facilities in other languages
that are sufficiently similar to Rust procedural macros and derive
in
particular.
Within Rust, the author has implemented support for generic trait impl
parameters in a custom attribute macro, before realizing that being able to
plug it into derive
would make it more intuitive to the users and take away
some of the parsing complexity from the macro.
Unresolved questions
- Is it advisable, or even possible syntactically, to extend the general
derive
syntax with optional generics for each comma-separated item, or should this be only permitted as an alternative form ofderive
with a single item? An alternative combining syntax#[derive(<T: Bound> Trait1 + Trait2 + Trait3)]
is also possible, either as a single item or part of a comma-separated list. - Should it be permitted to have two derive macros in scope for the
same trait, one with a
proc_macro_derive_with_generics
entry point and the other with a plainproc_macro_derive
?
Future possibilities
Extending derive
with generics would open this language extension mechanism
to far wider use and experimentation than what is possible today; the
motivation section provides only a few profitable examples.