On the unnameability of compiler-generated types

Is there any way to name the statically known compiler-generated types produced by closures and generators? Perhaps via macros to generate a compatible type?

Consider the following very simple (contrived) example. By inlining the definition of test1 I get the desired behaviour. Clearly the compiler generates a generic type C<..> with the appropriate impl<T0 : Clone> Clone for C<T0>.

fn test1<T>(t : T) -> impl FnOnce()
{
  move || {
    let _t = t;
  }
}

fn main()
{
  {
    let a = test1(0);
    let _b = a.clone(); // <-- compiler error
  }

  {
    let a = {
      let t = 0;
      move || {
        let _t = t;
      }
    };
    let _b = a.clone();
  }
}

I don't want existential/opaque types via impl Trait as in the example; and I don't want an erased closure type (see namable_closures). So far the only solution I can see is to replace all functions like test1 with macros (obviously, this would be a nightmare).

Your misunderstanding might be that you can only use in impl Trait what you have in Trait. This compiles.

To answer the original question: no, I don't think you can name the unnameable types. The only way to get to those types is in generics, but you won't get any useful "type name" out of them.

1 Like

"Type alias impl trait" (TAIT) is how you will be able to name opaque types, but it isn't stable yet. And it still requires you to specify all of the traits that the type implements that will be usable on the type.

1 Like

Yes, I suppose there's one more option: to enumerate all the possible permutations of the automatically implemented traits which occur for generated types.

But this too is a nightmare:

fn test1_cloneable<T : Clone>(t : T) -> impl FnOnce() + Clone
{
  move || {
    let _t = t;
  }
}

fn main()
{
  {
    let a = test1_cloneable(0);
    let _b = a.clone(); // yay, we can clone now!
  }

  struct X{}

  {
    // let a = test1_cloneable(X{}); // doesn't compile
  }

  {
    let a = {
      let t = X{};
      move || {
        let _t = t;
      }
    };
  }
}

TAIT adds no more useful semantics to this case. It lets you get the compiler to check the fact that multiple usages of an opaque type are in fact the same type; this isn't possible with closure types anyways. It lets you give a name to an "existential" type - this is useful, but unrelated to my problem.

Therein lies the problem: the precise trait which constraints the output type is something like

type Test1<T> = impl FnOnce() 
  : for <T : Clone>  Clone // the existential which we're naming has an `impl<T : Clone> Clone for C`
  + for <T : Sync> Sync  // .. likewise for Sync
  // + etc .. ;

fn test1<T>(t : T) -> Test1<T> { ... }

but this is not valid syntax (nor is the Rust type system strong enough to express these facts for an 'existential' type).

Well, if you want that, then you can just define conditional impls bounded by each required trait. I'd need to know what higher goal you are trying to achieve, but in the lack of that, I can still give a demonstrative (although likely not immediately useful) example with a simple newtype wrapper around a callable:

struct Function<F>(F);

impl<F: Clone> Clone for Function<F> {
    fn clone(&self) -> Self {
        Function(self.0.clone())
    }
}

impl<F: Copy> Copy for Function<F> {}

(Incidentally, these are exactly what the corresponding derives would have generated, I just wanted to make the explicit connection.)

How would I use a compiler generated type (closure) with this Function struct?

I want to clone a compiler-generated type (closures & generators), and access all the compiler-generated implementations, and return that type by value from a function e.g. test1, and have another function test2 which can call test1 and return another, different (non-compiler generated) type which refers to test1,

In other words I want to generate code like this:

#[magic_goes_here]
fn test1<T>(t : T) -> type Test1<T>
{
  move || {
    let _t = t;
  }
}

becomes (via macro expansion magic)

struct Test1<T>
{
  t : T
}

impl<T : Clone> Clone for Test1<T> { /* .. */ }

fn test1<T>(t : T) -> Test1<T>
{
  Test1{ t }
}

impl<T> FnOnce for Test1<T>
{
  extern "rust-call" fn call_once(self, args: Args) -> Self::Output {
    let _t = self.t;
  }
}

and then I can write things like


fn ex1() -> Vec<Test1<i32>>
{
  vec![ test1(0), test1(1) ]
}

fn ex2()
{
  let x = ex1();
  x[0]();
  x[1].clone();
}

struct X{} // note: not Clone
fn ex3() -> Vec<Test1<X>>
{
  vec![ test1(X{}), test1(X{}) ];
}

You wouldn't; this is why I was telling you I don't know what you are trying to do. I was merely trying to demonstrate how you can conditionally implement traits based on another type conditionally implementing the same traits.

As for your actual question, I'm not exactly sure what prevents you from doing it. After fixing the various syntax errors, your code compiles as-is (on nightly).

I should also state: the transformation done in magic_goes_here is theoretically doable, but would mean replicating a ton of work done by the Rust compiler, least of all, the subtle capturing rules of Rust closures. The same transformation for generators would be even more complex (and almost certainly requires unsafe in the generated code).

So rather than try to re-implement all the compiler does here, I would simply like to rely on it to do the heavy lifting. The only problem with the compiler generated type is that I cannot name it (otherwise, it has literally all of the semantics that I want!), so I cannot even write the types of ex1 or ex3.

Nothing, writing it all out manually is the best solution I could come up with presently.

This means I cannot use closure syntax and I cannot rely on implicit capturing rules. These are fairly minor. The worst, I have to do the generator translation from yield to a state machine manually. I have to apply the reasoning done by the compiler to determine if my generator is Unpin.

But compilers are tools meant to help me write programs. This is a bit like asking, "what prevents you from simply writing it in assembly/C/C++"? Nothing, it's just harder.

Ah, I thought you already have the implementation for that attribute macro. In this case, the way you would do it is probably still impl Trait. I'll try to write up an example.

1 Like

Opaque return position impl trait (-> impl Trait) types are leaky around auto-traits like Sync, which is effectively your + for<T: Sync> Sync wish. But not for other things.

Stuff you probably know

It's intentional that non-auto traits don't leak, as one of the benefits of being opaque is the freedom to change the concrete type later on without breaking downstream (by e.g. no longer implementing a trait which you never promised you would implement).

And the conditional opaque impl trait does not exist, as you note, so you can't make a conditional promise there.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.