Pre-RFC: Derives on anonymous types

Summary

This RFC allows #[derive] attributes to be used on closures and the experimental generators.

Motivation

The primary motivation comes from a case where you want to serialize a generator state machine; a manually coded state machine can be used with derive but the compiler generated one is not.

This also acts as an alternative to the recently stabilized automatic Copy/Clone on closures.

Explanation

You can do arbitrary derives on closures and generators:

let closure = #[derive(Clone, Serialize)] || {};

When passed to a custom derive, we will make two minimal guarantees:

  • A closure is a struct, and a generator is an enumeration.
  • There’s no hidden external states. Which means, the compiler does not add any global state or interior mutability on its own.

Unresolved questions

  • This conflicts with what we have done with copy/clone_closures. Maybe introduce a deprecation list and change the behaviour with editions?
  • This needs some syntax discussion if we merged async fn which syntactically looks like a fn while returning a generator.
  • Can we provide stronger guarantee for the fields, for situations like migrating serialized data?
1 Like

What if we spelled that as something along the lines of:

let closure: impl FnOnce() + Clone + Serialize = || {};
2 Likes

That seems backwards- currently impl Trait can’t add any new impls to the actual underlying type.

1 Like

In this case, it seems like the closure will already need to satisfy Copy and/or Clone, which makes derive seems odd to me. Using impl would explicitly state the expectation that the closure already meets those requirements.

Or have I misunderstood what the derive would do?

1 Like

Per the unresolved questions, I’m planning to remove the auto/implicit Clone/Copy impl.

The initial post describes this as “an alternative to the recently stabilized automatic Copy/Clone on closures.” I don’t think this makes sense- it’s already stable, the change isn’t worth an edition, and the current behavior matches other literals.

But presumably in other cases derive would do what it always does- create a new impl for the named trait. The initial post doesn’t go into any detail on exactly how this would work—anonymous types are not something we can just convert to a TokenStream to hand to custom derive, for example—but it seems that was the intention.

what is the proc macro passed? the syntax of the closure or something else?

1 Like

I don’t see how the macro for Serialize could possibly work without getting type information about the closure. And as far as I know there are no plans to support such macros.

1 Like

There are serious implementation blockers to supporting something like this. The actual structure of the anonymous type for both closures and generators is not generated until well into MIR, whereas derives are run during parsing. It would be a major refactor, maybe impossible, to generate this information prior to running the derives, because the information depends on type information, and typecking in general could depend on the code generated by the derive.

4 Likes

The point is, if we can derive code from macros to add additional traits to closure types, should we also be able to add those traits explicitly?

If we could, this will then give us an obvious way to implement those macros, because all the macros have to do is to convert it to an explicit trait.

But this is boiled down to my previous idea: something similar to Java’s anonymous class.

So the following

let closure = #[derive(Clone, Serialize)] || {};

can be converted to

let closure = struct { 
      /* explicitly captured fields */
      /*Note: anynymous types can only be structs, not enums*/
      /*Because this part should always be private and so the variant name will not be accessable*/
      /* this part can de derived automatically in human written code */
} :
impl Clone {
    fn clone(&self) { /*generated by macro*/ }
} +
impl Serialize {
   /* generated... */
} +
impl FnOnce() {
   type Output = ();
   //ignore "rust-call"
   fn call_once() {
       /* closure code */
   }
};
2 Likes

typeof syntax might be another option:

let closure = |...| { ... };

impl Serialize for typeof(closure) {
   /* generated... */
}

(I have no opinions on this thread yet, just throwing this out there)

3 Likes

Yes, this gives another way to add traits to anonymous. But it will requires anonymous object to always be callable closures. In my variant, you can have anonymous types that does not implement FnXXX traits.

I question whether this is really useful at all (ignoring the high implementation cost, for discussion’s sake)? Let’s look at the vanilla derivable types: Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, and Default.

  • Copy and Clone are super useful – so useful that we’ve already special-cased them in the compiler.
  • Debug is vaguely useful, for introspecting the captures? I’d like to see a compelling scenario in which which debug-printing a closure’s captures outside of the closure’s body is useful.
  • PartialEq and Eq are only useful if you’re comparing copies of the same closure by their captures. The least pathological example I can think of is if you had some kind of function like
fn foo(i: i32) -> impl Fn() -> i32 + Eq { move || i }

and tried to compare that they return the same integer… but even this seems pretty silly.

  • PartialOrd and Ord suffer from the same problem – plus, what order are the captures laid out in? This is currently unspecified, which means that the order is some non-canonical, unspecified ordering, which I am skeptical about.
  • Hash would primarially be useful for using closures as keys into a hash table… at which point you should be using opaque tuple structs anyways.
  • Default is essentially useless; to use it, you need a handle on the closure’s type… which you can’t get baring typeof or fn foo<T: Default>(_: T)… which defeat the purpose of Default in the first place.

The serde traits are also just as questionable, since you’d just be serializing the captures, for a type you can’t even name. How do you deserialize something like that? It’s not like you’ll be able to serialize code to send over the network to do RPCs, since the code inside a closure is part of its type. (I admit I am not an expert on how serde works, so feel free to strike that entire paragraph.)

tl;dr I question whether allowing closures to implement complicated traits based on their captures is a good design choice at all.

1 Like

Another syntax that has been floated around (at least by @nikomatsakis and I, although I believe there was more I have forgotten):

struct {
    a, b, c
} impl Foo  {
    fn foo(&mut self) {
        self.a += self.b * self.c;
    }
}

Another similar syntax is getting rid of the struct {...} and just relying on captures, just like closures.

That would make |x| x + y effectively behave as is if it was desugared into:

impl FnOnce(_) -> _ {
    fn call_once(&self, (x,): (_,)) -> _ {
        x + y /* maybe `self.y`? */
    }
}

Note that despite this looking like a nested item, it’s not, instead it’s more like Java anonymous classes, and would share the “shared type-checking with parent function” property of Rust closures.

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.