Pre-RFC: Anonymous Trait Impls

Hey there, I was just talking briefly in the Rust discord about ideas for an anonymous trait impl concept, here's what we have so far.

Sample syntaxes actively in-debate:

fn create_trait(id: u32) -> impl Trait {
    return impl trait { .. }
    return impl trait Trait { .. } // optional type hint
    return move impl { .. }
    return move impl trait { .. }
    return struct impl Trait { .. }
    return struct impl { .. }
}

impl Trait creates ambiguity about whether it is an expression or an item.

It'd work pretty similarly to closures capturing variables, ex:

let x = 1;
// create the closure
let my_closure = move || { println!("{}", x); };
// call the closure
my_closure();

equivalent to:

struct Anonymous {
    x: i32,
}
impl Anonymous {
    fn call_the_thing(&self) {
        println!("{}", self.x);
    }
}

let x = 1;
// create the closure
let my_closure = Anonymous { x: x };
// call the closure
my_closure.call_the_thing();

So far we've discussed:

  1. Instantiation Syntax
    let x = impl Trait { ... };
    No let x = impl Trait for struct { var1, var2 } { ... }; or anything similar. This anonymous impl Trait syntax is meant to be a shorter & simpler version of a regular trait impl with reduced complexity. If any further complexity is needed the user can always just convert it into a conventional item with trait impl.
  2. How to create Self
    As the field specifics are left unspecified to the compiler, there wouldn't be a way to construct Self { ... } within the anonymous trait impl. I am aware that this is a restriction; however I'd like to highlight that 1. Closures cannot be constructed in the body of themselves. 2. If you need to construct the anonymous trait impl; you can always make a wrapper function to instantiate the anonymous trait, ex:
fn create_trait(id: u32) -> impl Trait {
    let x = impl Trait {
        fn id(&self) -> u32 { id }
        fn recreate(&self) -> Self { create_trait(id) }
    };
    x
}

fn do_something(user: User) {
   let x = create_trait(user.id);
}
  1. Multiple Trait Bounds (impl Trait1 + impl Trait2)
fn foo() -> impl Trait1 + Trait2 {
    return impl { // Is this Trait1 or Trait2?
    }
}

Our decision here is to restrict these to be unrepresentable with anonymous traits. Additionally, traits such as Subtrait: Trait1 + Trait2 couldn't be implemented anonymously due to missing supertraits.

I am very aware that my markdown isn't fancy-looking or anything and I haven't structured this professionally, I can always edit this as we get more refined on what is actually needed and how we'd structure this for an RFC. (Luckily this is a RFC on a RFC lol)

2 Likes

Some prior related discussions:

2 Likes

Since Rust is principled about type inference stopping at function boundaries, and the anonymous trait is created inside said boundaries, I say we crank type inference up to the max:

fn create_trait(id: u32) -> impl Trait {
    move impl {
        fn id() { id }
        fn recreate(new_id) { create_trait(new_id) }
    }
}
  • impl instead of impl Trait because we can infer it from the return type. This means that we no longer need to bind it to a variable before returning it.
  • No type annotations for the trait methods' arguments and return types. These are determined by the trait definition - no need to repeat them.
  • No self. There is no "self" access, because the data is obtained closure-style. self is for accessing the "parent" value (like self inside a closure)
  • move is needed for the same reasons it's needed in closures.

If we are uncomfortable with having fns without all the annotations, we can also use struct-like syntax:

fn create_trait(id: u32) -> impl Trait {
    move impl {
        id: || { id },
        recreate: |new_id| { create_trait(new_id) },
    }
}

Where the main difference from a struct is that the closures share access to the data.

I'm a big fan of a lot of this. move impl seems much better. However I'm concerned with it being read as an impl and not impl trait. impl blocks exist on types, and with looking at the body only, it's not obvious to infer that move impl means impl Trait.

IMO I'd prefer move impl trait to fix this, however it is three keywords in a row and I'm also not a huge fan of that.

I think we need to cover self as a valid parameter. Interop with existing code and using anonymous trait impls becomes difficult if they supply a &self vs a &mut self parameter; or none at all. What we are allowed to do with this type and it's captures becomes different depending on the &self and &mut self parameter.

+ on the move tho.

I have some corner cases to consider:

fn foo() -> impl Trait1 + Trait2 {
    return impl { // Is this Trait1 or Trait2?
    }
}

If impl A + B is not allowed for non-trivial B, then trait C: A + B and use impl C instead.

1 Like

My preferred syntax is struct impl TraitName { .... }. This avoids any syntactic ambiguity since the token sequence struct impl is currently not in the language. And I think that someone who hasn't seen this syntax before is more likely to understand the meaning of this syntax than with other syntaxes.

2 Likes

Oh, there's a previous RFC for this. RFC: impl trait expressions by canndrew · Pull Request #2604 · rust-lang/rfcs · GitHub

It's "postponed". It might be time to make a new RFC for it?

3 Likes

I just updated the post contents; both cases would be restricted. The first wouldn't be allowed because it is not a singular trait bound. The second would not be allowed because an impl would be missing supertraits A and B.

Definitely, once I feel we've ironed it out well-enough here on the Pre-RFC.